Files
PR/.github/workflows/build-android.yml
T
2026-04-29 18:18:28 +12:00

336 lines
16 KiB
YAML

name: Build Citron (Android)
concurrency:
group: build-android-${{ inputs.target_branch || github.event.pull_request.head.ref || github.ref }}
cancel-in-progress: true
on:
schedule:
- cron: "0 * * * *"
workflow_dispatch:
inputs:
target_branch:
description: "Upstream branch to build"
required: false
type: string
pr_number:
description: "PR number to comment on"
required: false
type: string
head_repo:
description: "Head repo to clone from (e.g. user/emulator)"
required: false
type: string
env:
UPSTREAM_REPO: "citron-neo/emulator" # <--- Change this to switch forks
jobs:
check-version:
name: Check if build needed
runs-on: ubuntu-latest
outputs:
should_build: ${{ steps.compare.outputs.should_build }}
new_hash: ${{ steps.upstream.outputs.hash_short }}
new_hash_full: ${{ steps.upstream.outputs.hash_full }}
branch_name: ${{ steps.upstream.outputs.branch_name }}
head_repo: ${{ steps.upstream.outputs.head_repo }}
pr_number: ${{ steps.upstream.outputs.pr_number }}
steps:
- name: Checkout this repo
uses: actions/checkout@v4
- name: Get upstream commit hash
id: upstream
run: |
PR_NUMBER="${{ inputs.pr_number }}"
TARGET_BRANCH="${{ inputs.target_branch }}"
HEAD_REPO="${{ inputs.head_repo }}"
if [ -z "$HEAD_REPO" ]; then HEAD_REPO="${{ env.UPSTREAM_REPO }}"; fi
if [ -z "$TARGET_BRANCH" ]; then TARGET_BRANCH="main"; fi
REPO_URL="https://github.com/${HEAD_REPO}.git"
FULL_HASH=$(git ls-remote "$REPO_URL" "refs/heads/$TARGET_BRANCH" | cut -f1 | head -n1)
if [ -z "$FULL_HASH" ]; then
REPO_URL="https://github.com/${{ env.UPSTREAM_REPO }}.git"
TARGET_BRANCH="main"
FULL_HASH=$(git ls-remote "$REPO_URL" HEAD | cut -f1)
fi
SHORT_HASH=$(echo "$FULL_HASH" | cut -c1-7)
echo "branch_name=$TARGET_BRANCH" >> $GITHUB_OUTPUT
echo "hash_short=$SHORT_HASH" >> $GITHUB_OUTPUT
echo "hash_full=$FULL_HASH" >> $GITHUB_OUTPUT
echo "head_repo=$HEAD_REPO" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
- name: Check if already built
id: built_check
run: |
HASH="${{ steps.upstream.outputs.hash_full }}"
WORKFLOW_FILE="build-android.yml"
COUNT=$(gh api "repos/${{ github.repository }}/actions/workflows/${WORKFLOW_FILE}/runs?status=success&per_page=100" --jq "[.workflow_runs[] | select(.head_sha == \"${HASH}\") | select(.id != ${{ github.run_id }})] | length")
echo "already_built=$([ "$COUNT" -gt 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Compare versions
id: compare
run: |
if [ "${{ steps.built_check.outputs.already_built }}" = "true" ]; then
echo "should_build=false" >> $GITHUB_OUTPUT
else
echo "should_build=true" >> $GITHUB_OUTPUT
fi
comment-start:
name: Mark Progress
needs: [check-version]
if: needs.check-version.outputs.should_build == 'true' && needs.check-version.outputs.pr_number != ''
runs-on: ubuntu-latest
steps:
- name: Update Master Comment (In Progress)
env:
GH_TOKEN: ${{ secrets.EMULATOR_PR_TOKEN }}
PR_NUMBER: ${{ needs.check-version.outputs.pr_number }}
UPSTREAM: ${{ env.UPSTREAM_REPO }}
HASH_SHORT: ${{ needs.check-version.outputs.new_hash }}
HASH_FULL: ${{ needs.check-version.outputs.new_hash_full }}
HEAD_REPO: ${{ needs.check-version.outputs.head_repo }}
run: |
if [ -z "$GH_TOKEN" ]; then echo "EMULATOR_PR_TOKEN not set"; exit 0; fi
MARKER="<!-- citron-pr-builds:${PR_NUMBER} -->"
TITLE="**Build Artifacts for PR #${PR_NUMBER}**"
COMMENT_IDS=$(gh api "repos/${UPSTREAM}/issues/${PR_NUMBER}/comments?per_page=100" --jq ".[] | select(.body | contains(\"$MARKER\") or contains(\"Build Artifacts for PR #${PR_NUMBER}\")) | .id" 2>/dev/null || true)
COMMENT_ID=$(printf '%s\n' "$COMMENT_IDS" | sed '/^$/d' | tail -n 1)
if [ -z "$COMMENT_ID" ]; then
INITIAL_BODY="${MARKER}
${TITLE}
| Platform | Commit | Artifacts (Direct Download) | Logs |
| :--- | :--- | :--- | :--- |
| **Windows** | | Pending | |
| **Linux** | | Pending | |
| **Android** | | Pending | |
| **macOS** | | Pending | |
*Note: Direct downloads powered by nightly.link*"
jq -n --arg body "$INITIAL_BODY" '{body: $body}' | gh api -X POST "repos/${UPSTREAM}/issues/${PR_NUMBER}/comments" --input - >/dev/null || echo "Failed creation"
sleep 2
COMMENT_IDS=$(gh api "repos/${UPSTREAM}/issues/${PR_NUMBER}/comments?per_page=100" --jq ".[] | select(.body | contains(\"$MARKER\") or contains(\"Build Artifacts for PR #${PR_NUMBER}\")) | .id" 2>/dev/null || true)
COMMENT_ID=$(printf '%s\n' "$COMMENT_IDS" | sed '/^$/d' | tail -n 1)
fi
for DUPLICATE_ID in $COMMENT_IDS; do
if [ -n "$COMMENT_ID" ] && [ "$DUPLICATE_ID" != "$COMMENT_ID" ]; then
gh api -X DELETE "repos/${UPSTREAM}/issues/comments/${DUPLICATE_ID}" >/dev/null 2>&1 || true
fi
done
if [ -n "$COMMENT_ID" ]; then
COMMIT_LINK="[\`${HASH_SHORT}\`](https://github.com/${HEAD_REPO}/commit/${HASH_FULL})"
export NEW_ROW="| **Android** | ${COMMIT_LINK} | Building... | |"
gh api "repos/${UPSTREAM}/issues/comments/${COMMENT_ID}" --jq ".body" > body.txt
grep -Fq "$MARKER" body.txt || { BODY=$(cat body.txt); printf '%s\n%s\n' "$MARKER" "$BODY" > body.txt; }
perl -pi -e 's|.*\*\*Android\*\*.*|$ENV{NEW_ROW}|' body.txt
jq -n --arg body "$(cat body.txt)" '{body: $body}' | gh api -X PATCH "repos/${UPSTREAM}/issues/comments/${COMMENT_ID}" --input - || echo "Failed update"
fi
continue-on-error: true
build-android:
name: Build Android APK
needs: [check-version, comment-start]
if: always() && needs.check-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_COMPILERCHECK: content
CCACHE_SLOPPINESS: time_macros
steps:
- name: Clone Citron Source
run: |
CLONE_REPO="${{ needs.check-version.outputs.head_repo }}"
if [[ "$CLONE_REPO" != *"/"* ]]; then CLONE_REPO="${{ env.UPSTREAM_REPO }}"; fi
git clone --recursive -b "${{ needs.check-version.outputs.branch_name }}" "https://github.com/${CLONE_REPO}.git" citron_source
- name: Install Build Dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ccache glslang-tools libvulkan-dev python3-requests patch
- name: Setup and Restore Cache
uses: actions/cache@v4
id: ccache-cache
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-android-ccache-standard-${{ github.ref_name }}-${{ hashFiles('citron_source/**/CMakeLists.txt') }}
restore-keys: |
${{ runner.os }}-android-ccache-standard-${{ github.ref_name }}-
${{ runner.os }}-android-ccache-standard-
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq wget unzip curl git cmake build-essential pkg-config zip glslang-tools nasm perl autoconf automake libtool yasm
- name: Install Android SDK & NDK
run: |
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
unzip -q commandlinetools-linux-11076708_latest.zip
mkdir -p android-sdk/cmdline-tools; mv cmdline-tools android-sdk/cmdline-tools/latest
export ANDROID_HOME=$PWD/android-sdk
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
yes | sdkmanager --licenses > /dev/null || true
sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" "ndk;26.1.10909125"
echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125" >> $GITHUB_ENV
- name: Build APK
working-directory: ./citron_source/src/android
run: |
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export ANDROID_NDK="$ANDROID_NDK_HOME"
chmod +x gradlew
./gradlew assembleMainlineRelease -Pcmake.args="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DENABLE_QT=OFF -DENABLE_SDL2=OFF -DENABLE_WEB_SERVICE=OFF -DENABLE_OPENSSL=OFF -DCITRON_USE_BUNDLED_VCPKG=ON -DCITRON_USE_BUNDLED_FFMPEG=ON -DCITRON_ENABLE_LTO=ON -DCITRON_BUILD_TYPE=Release"
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: citron-android-standard
path: citron_source/src/android/app/build/outputs/apk/mainline/release/*.apk
build-android-8elite:
name: Build Android APK (Snapdragon 8 Elite)
needs: [check-version, comment-start]
if: always() && needs.check-version.outputs.should_build == 'true'
runs-on: ubuntu-latest
env:
TARGET: Lyb
MODEL: "8 Elite"
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_COMPILERCHECK: content
CCACHE_SLOPPINESS: time_macros
steps:
- name: Clone Citron Source
run: |
CLONE_REPO="${{ needs.check-version.outputs.head_repo }}"
if [[ "$CLONE_REPO" != *"/"* ]]; then CLONE_REPO="${{ env.UPSTREAM_REPO }}"; fi
git clone --recursive -b "${{ needs.check-version.outputs.branch_name }}" "https://github.com/${CLONE_REPO}.git" citron_source
- name: Install Build Dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ccache glslang-tools libvulkan-dev python3-requests patch
- name: Setup and Restore Cache
uses: actions/cache@v4
id: ccache-cache-8elite
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-android-ccache-${{ env.TARGET }}-${{ github.ref_name }}-${{ hashFiles('citron_source/**/CMakeLists.txt') }}
restore-keys: |
${{ runner.os }}-android-ccache-${{ env.TARGET }}-${{ github.ref_name }}-
${{ runner.os }}-android-ccache-${{ env.TARGET }}-
- name: Apply 8 Elite Patches
working-directory: ./citron_source
run: |
sed -i '/if (!handle) {/,/}/c\ if (!handle) {\n handle = dlopen("libvulkan.so", RTLD_NOW);\n }' src/android/app/src/main/jni/native.cpp
sed -i '/bool IsShaderInt64Supported() const {/a \ const auto driver = GetDriverID();\n if (driver == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) {\n return false;\n }' src/video_core/vulkan_common/vulkan_device.h
sed -i '/bool IsExtShaderAtomicInt64Supported() const {/a \ const auto driver = GetDriverID();\n if (driver == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) {\n return false;\n }' src/video_core/vulkan_common/vulkan_device.h
sed -i '/const auto device_id =/d' src/video_core/vulkan_common/vulkan_device.cpp
sed -i 's/\bdevice_id\b/properties.properties.deviceID/g' src/video_core/vulkan_common/vulkan_device.cpp
sed -i 's/if (is_qualcomm) {/if (driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY) {/' src/video_core/vulkan_common/vulkan_device.cpp
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Install Android SDK & NDK
run: |
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
unzip -q commandlinetools-linux-11076708_latest.zip
mkdir -p android-sdk/cmdline-tools; mv cmdline-tools android-sdk/cmdline-tools/latest
export ANDROID_HOME=$PWD/android-sdk
export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
yes | sdkmanager --licenses > /dev/null || true
sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0" "ndk;26.1.10909125"
echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125" >> $GITHUB_ENV
- name: Build APK (8 Elite)
working-directory: ./citron_source/src/android
run: |
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export ANDROID_NDK="$ANDROID_NDK_HOME"
chmod +x gradlew
./gradlew assembleMainlineRelease -Pcmake.args="-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DENABLE_QT=OFF -DENABLE_SDL2=OFF -DENABLE_WEB_SERVICE=OFF -DENABLE_OPENSSL=OFF -DCITRON_USE_BUNDLED_VCPKG=ON -DCITRON_USE_BUNDLED_FFMPEG=ON -DCITRON_ENABLE_LTO=ON -DCITRON_BUILD_TYPE=Release"
- name: Upload APK (8 Elite)
uses: actions/upload-artifact@v4
with:
name: citron-android-8elite
path: citron_source/src/android/app/build/outputs/apk/mainline/release/*.apk
comment-end:
name: Comment End
needs: [check-version, build-android, build-android-8elite]
if: always() && needs.check-version.outputs.pr_number != ''
runs-on: ubuntu-latest
steps:
- name: Update Master Comment (Results)
env:
GH_TOKEN: ${{ secrets.EMULATOR_PR_TOKEN }}
PR_NUMBER: ${{ needs.check-version.outputs.pr_number }}
RUN_ID: ${{ github.run_id }}
REPO: ${{ github.repository }}
UPSTREAM: ${{ env.UPSTREAM_REPO }}
HASH_SHORT: ${{ needs.check-version.outputs.new_hash }}
HASH_FULL: ${{ needs.check-version.outputs.new_hash_full }}
HEAD_REPO: ${{ needs.check-version.outputs.head_repo }}
STD_STATUS: ${{ needs.build-android.result }}
ELITE_STATUS: ${{ needs.build-android-8elite.result }}
run: |
if [ -z "$GH_TOKEN" ]; then echo "EMULATOR_PR_TOKEN not set"; exit 1; fi
MARKER="<!-- citron-pr-builds:${PR_NUMBER} -->"
COMMENT_IDS=$(gh api "repos/${UPSTREAM}/issues/${PR_NUMBER}/comments?per_page=100" --jq ".[] | select(.body | contains(\"$MARKER\") or contains(\"Build Artifacts for PR #${PR_NUMBER}\")) | .id" 2>/dev/null || true)
COMMENT_ID=$(printf '%s\n' "$COMMENT_IDS" | sed '/^$/d' | tail -n 1)
for DUPLICATE_ID in $COMMENT_IDS; do
if [ -n "$COMMENT_ID" ] && [ "$DUPLICATE_ID" != "$COMMENT_ID" ]; then
gh api -X DELETE "repos/${UPSTREAM}/issues/comments/${DUPLICATE_ID}" >/dev/null 2>&1 || true
fi
done
if [ -n "$COMMENT_ID" ]; then
LOG_URL="https://github.com/${REPO}/actions/runs/${RUN_ID}"
COMMIT_LINK="[\`${HASH_SHORT}\`](https://github.com/${HEAD_REPO}/commit/${HASH_FULL})"
if [ "$STD_STATUS" = "success" ] && [ "$ELITE_STATUS" = "success" ]; then
STD_URL="https://nightly.link/${REPO}/actions/runs/${RUN_ID}/citron-android-standard.zip"
ELITE_URL="https://nightly.link/${REPO}/actions/runs/${RUN_ID}/citron-android-8elite.zip"
ARTIFACTS="[Standard](${STD_URL}) [8 Elite](${ELITE_URL})"
else
ARTIFACTS="Failed"
fi
export NEW_ROW="| **Android** | ${COMMIT_LINK} | ${ARTIFACTS} | [Run](${LOG_URL}) |"
gh api "repos/${UPSTREAM}/issues/comments/${COMMENT_ID}" --jq ".body" > body.txt
grep -Fq "$MARKER" body.txt || { BODY=$(cat body.txt); printf '%s\n%s\n' "$MARKER" "$BODY" > body.txt; }
perl -pi -e 's|.*\*\*Android\*\*.*|$ENV{NEW_ROW}|' body.txt
jq -n --arg body "$(cat body.txt)" '{body: $body}' | gh api -X PATCH "repos/${UPSTREAM}/issues/comments/${COMMENT_ID}" --input - || echo "Failed update"
else
echo "Master comment not found, skipping final update."
fi