Files
PR/.github/workflows/build-linux.yml
T

326 lines
14 KiB
YAML

name: Build Citron (Linux)
concurrency:
group: build-linux-${{ inputs.pr_number || inputs.target_branch || github.ref }}
on:
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
echo "::error::Could not resolve branch '${TARGET_BRANCH}' in '${HEAD_REPO}'. Refusing to fall back to main."
exit 1
fi
SHORT_HASH=$(echo "$FULL_HASH" | cut -c1-7)
echo "Resolved ${HEAD_REPO}@${TARGET_BRANCH} to ${FULL_HASH}"
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 (via PR comment)
id: built_check
env:
GH_TOKEN: ${{ secrets.EMULATOR_PR_TOKEN }}
run: |
HASH_SHORT="${{ steps.upstream.outputs.hash_short }}"
PR_NUMBER="${{ steps.upstream.outputs.pr_number }}"
UPSTREAM="${{ env.UPSTREAM_REPO }}"
PLATFORM="Linux"
if [ -z "$GH_TOKEN" ] || [ -z "$PR_NUMBER" ]; then
echo "No token or PR number, will build."
echo "already_built=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Checking for HASH_SHORT: $HASH_SHORT"
MARKER="<!-- citron-pr-builds:${PR_NUMBER} -->"
COMMENT_BODY=$(gh api "repos/${UPSTREAM}/issues/${PR_NUMBER}/comments?per_page=100" \
--jq "[.[] | select(.body | contains(\"$MARKER\") or contains(\"Build Artifacts for PR #${PR_NUMBER}\"))] | last | .body // empty" \
2>/dev/null || echo "")
if [ -z "$COMMENT_BODY" ]; then
echo "No master comment found, will build."
echo "already_built=false" >> $GITHUB_OUTPUT
exit 0
fi
PLATFORM_ROW=$(echo "$COMMENT_BODY" | grep "\*\*${PLATFORM}\*\*" || true)
echo "Platform row: $PLATFORM_ROW"
if [ -n "$PLATFORM_ROW" ] && echo "$PLATFORM_ROW" | grep -qF "$HASH_SHORT"; then
if echo "$PLATFORM_ROW" | grep -qE "(\[x86_64\]|\[v3\]|\[ARM\]|Success|Failed)"; then
echo "Commit ${HASH_SHORT} already handled for ${PLATFORM}. Skipping."
echo "already_built=true" >> $GITHUB_OUTPUT
exit 0
elif echo "$PLATFORM_ROW" | grep -q "Building..."; then
echo "Commit ${HASH_SHORT} is currently building for ${PLATFORM}. Will build again to be safe."
echo "already_built=false" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "No conclusive result for commit ${HASH_SHORT} on ${PLATFORM}. Will build."
echo "already_built=false" >> $GITHUB_OUTPUT
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="| **Linux** | ${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|.*\*\*Linux\*\*.*|$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:
name: "${{ matrix.name }}"
needs: [check-version, comment-start]
if: always() && needs.check-version.outputs.should_build == 'true'
runs-on: ${{ matrix.os }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- name: "Citron Build (Normal x86_64)"
os: ubuntu-latest
artifact_name: "Artifacts_x86_64"
script_suffix: ""
build_arg: ""
- name: "Citron Build (Optimized x86_64)"
os: ubuntu-latest
artifact_name: "Artifacts_v3"
script_suffix: "_v3"
build_arg: "v3"
- name: "Citron Build (aarch64)"
os: ubuntu-24.04-arm
artifact_name: "Artifacts_aarch64"
script_suffix: ""
build_arg: ""
container: ghcr.io/pkgforge-dev/archlinux:latest
steps:
- name: Install Git
run: pacman -Syu --noconfirm --needed git
- name: Checkout Workflow Scripts
uses: actions/checkout@v4
- name: Clone Citron Source Code
shell: bash
run: |
CLONE_REPO="${{ needs.check-version.outputs.head_repo }}"
BRANCH_NAME="${{ needs.check-version.outputs.branch_name }}"
EXPECTED_HASH="${{ needs.check-version.outputs.new_hash_full }}"
if [ -z "$CLONE_REPO" ] || [[ "$CLONE_REPO" != *"/"* ]]; then CLONE_REPO="${{ env.UPSTREAM_REPO }}"; fi
if [ -z "$BRANCH_NAME" ]; then BRANCH_NAME="main"; fi
echo "Cloning from $CLONE_REPO (branch: $BRANCH_NAME)..."
git clone --recurse-submodules --shallow-submodules --depth 1 -b "$BRANCH_NAME" "https://github.com/${CLONE_REPO}.git" emulator
cd emulator
ACTUAL_HASH=$(git rev-parse HEAD)
echo "Checked out ${CLONE_REPO}@${BRANCH_NAME}: ${ACTUAL_HASH}"
if [ -n "$EXPECTED_HASH" ] && [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then
echo "::error::Checked out ${ACTUAL_HASH}, expected ${EXPECTED_HASH}"
exit 1
fi
- name: Prepare Build Environment
run: |
mv get-dependencies.sh emulator/
mv build-citron.sh emulator/
mv package-citron.sh emulator/
- name: Add Git Safe Directory
working-directory: ./emulator
run: git config --global --add safe.directory "$PWD"
- name: Install dependencies
working-directory: ./emulator
run: chmod +x ./get-dependencies.sh && ./get-dependencies.sh
- name: Build Citron from source
working-directory: ./emulator
run: |
NPROC_VAL=$(nproc --all)
JOBS_VAL=$((NPROC_VAL > 4 ? 4 : NPROC_VAL))
chmod +x ./build-citron.sh
JOBS=${JOBS_VAL} DEVEL=true ./build-citron.sh ${{ matrix.build_arg }}
- name: Package AppImage with Correct Name
working-directory: ./emulator
run: |
GIT_SHA=$(git rev-parse --short HEAD)
PR_NUMBER="${{ needs.check-version.outputs.pr_number }}"
BRANCH_NAME="${{ needs.check-version.outputs.branch_name }}"
SAFE_BRANCH=$(printf '%s' "$BRANCH_NAME" | tr '/ ' '--' | tr -cd 'A-Za-z0-9._-')
if [ -n "$PR_NUMBER" ]; then
export APP_VERSION="PR-${PR_NUMBER}-${GIT_SHA}"
else
export APP_VERSION="${SAFE_BRANCH:-main}-${GIT_SHA}"
fi
export VERSION="${APP_VERSION}"
export ARCH_SUFFIX="${{ matrix.script_suffix }}"
export DEVEL="true"
chmod +x ./package-citron.sh
./package-citron.sh
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact_name }}
path: emulator/dist/
comment-end:
name: Comment End
needs: [check-version, build]
if: always() && needs.check-version.outputs.should_build == 'true' && 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 }}
BUILD_STATUS: ${{ needs.build.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 [ "$BUILD_STATUS" = "success" ]; then
X86_URL="https://nightly.link/${REPO}/actions/runs/${RUN_ID}/Artifacts_x86_64.zip"
V3_URL="https://nightly.link/${REPO}/actions/runs/${RUN_ID}/Artifacts_v3.zip"
ARM_URL="https://nightly.link/${REPO}/actions/runs/${RUN_ID}/Artifacts_aarch64.zip"
ARTIFACTS="[x86_64](${X86_URL}) [v3](${V3_URL}) [ARM](${ARM_URL})"
else
ARTIFACTS="Failed"
fi
export NEW_ROW="| **Linux** | ${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|.*\*\*Linux\*\*.*|$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