Skip to content

Runtime Examples

Runtime Examples #82

name: Runtime Examples
on:
workflow_run:
workflows:
- Build Snapshot
types:
- completed
branches:
- master
workflow_dispatch:
permissions:
actions: write
contents: read
jobs:
desktop-c-linux:
name: ${{ matrix.example.label }} desktop-c Linux runtime
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
example:
- id: basic
label: Basic
executable: app
- id: freetype
label: FreeType
executable: freetype
- id: controllers
label: Controllers
executable: controllers
env:
APP_PATH: examples/${{ matrix.example.id }}/platforms/desktop/teavm-c/builder/build/dist/c/release/${{ matrix.example.executable }}_debug
BUILD_TASK: :examples:${{ matrix.example.id }}:platforms:desktop:teavm-c:builder:${{ matrix.example.id }}_desktop_c_debug_build
PROOF_DIR: runtime-proof/linux/${{ matrix.example.id }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
- name: Install native build and runtime dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
ffmpeg \
libgl1-mesa-dev \
libwayland-dev \
libxkbcommon-dev \
mesa-utils \
pkg-config \
x11-apps \
xorg-dev \
xvfb
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Build desktop-c debug executable
run: |
mkdir -p "$PROOF_DIR"
./gradlew "$BUILD_TASK" --stacktrace 2>&1 | tee "$PROOF_DIR/build.log"
exit "${PIPESTATUS[0]}"
- name: Verify GLFW and GLEW are statically linked
run: |
set -euo pipefail
APP_ABS="$GITHUB_WORKSPACE/$APP_PATH"
ldd "$APP_ABS" | tee "$PROOF_DIR/native-dependencies.txt"
if grep -Eiq 'lib(glfw|glew)' "$PROOF_DIR/native-dependencies.txt"; then
echo "The Linux executable still depends on a GLFW or GLEW shared library."
exit 1
fi
- name: Run and capture proof
env:
DISPLAY: :99
LIBGL_ALWAYS_SOFTWARE: 1
run: |
set -euo pipefail
mkdir -p "$PROOF_DIR"
Xvfb "$DISPLAY" -screen 0 1280x720x24 +extension GLX +render -noreset > "$PROOF_DIR/xvfb.log" 2>&1 &
XVFB_PID=$!
cleanup() {
if [ -n "${APP_PID:-}" ] && kill -0 "$APP_PID" 2>/dev/null; then
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
fi
if kill -0 "$XVFB_PID" 2>/dev/null; then
kill "$XVFB_PID" 2>/dev/null || true
wait "$XVFB_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
sleep 2
glxinfo -B > "$PROOF_DIR/graphics-info.txt" 2>&1 || true
APP_ABS="$GITHUB_WORKSPACE/$APP_PATH"
APP_DIR="$(dirname "$APP_ABS")"
APP_NAME="$(basename "$APP_ABS")"
chmod +x "$APP_ABS"
pushd "$APP_DIR"
"./$APP_NAME" > "$GITHUB_WORKSPACE/$PROOF_DIR/app.log" 2>&1 &
APP_PID=$!
popd
sleep 8
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "${{ matrix.example.label }} desktop-c process exited before proof capture."
cat "$PROOF_DIR/app.log" || true
exit 1
fi
ffmpeg -y -f x11grab -video_size 1280x720 -i "$DISPLAY.0" -frames:v 1 "$PROOF_DIR/screenshot.png"
ffmpeg -y -f x11grab -video_size 1280x720 -framerate 15 -i "$DISPLAY.0" -t 5 "$PROOF_DIR/video.mp4" || true
test -s "$PROOF_DIR/screenshot.png"
- name: Collect FPS log
if: always()
run: |
FPS_SOURCE="$(dirname "$GITHUB_WORKSPACE/$APP_PATH")/fps.log"
if [ -s "$FPS_SOURCE" ]; then
cp "$FPS_SOURCE" "$PROOF_DIR/fps.log"
cat "$PROOF_DIR/fps.log"
else
echo "::error title=Missing FPS log::${{ matrix.example.label }} desktop-c Linux did not write fps.log"
exit 1
fi
- name: Upload runtime proof
uses: actions/upload-artifact@v7
if: always()
with:
name: ${{ matrix.example.id }}-desktop-c-linux-runtime-proof
path: runtime-proof
if-no-files-found: warn
overwrite: true
desktop-c-macos:
name: ${{ matrix.example.label }} desktop-c macOS runtime
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: macos-15
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
example:
- id: basic
label: Basic
executable: app
- id: freetype
label: FreeType
executable: freetype
- id: controllers
label: Controllers
executable: controllers
env:
APP_PATH: examples/${{ matrix.example.id }}/platforms/desktop/teavm-c/builder/build/dist/c/release/${{ matrix.example.executable }}_debug
GENERATE_TASK: :examples:${{ matrix.example.id }}:platforms:desktop:teavm-c:builder:${{ matrix.example.id }}_desktop_c_generate
PROOF_DIR: runtime-proof/macos/${{ matrix.example.id }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
- name: Prepare GLFW macOS software renderer retry source
run: |
set -euo pipefail
GLFW_SRC="$RUNNER_TEMP/glfw"
git init "$GLFW_SRC"
git -C "$GLFW_SRC" remote add origin https://github.com/glfw/glfw.git
git -C "$GLFW_SRC" fetch --depth 1 origin pull/2571/head
git -C "$GLFW_SRC" checkout FETCH_HEAD
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Record runner info
run: |
mkdir -p "$PROOF_DIR"
{
sw_vers
uname -a
sysctl -n machdep.cpu.brand_string
brew --prefix
cmake --version
clang --version
} > "$PROOF_DIR/system-info.txt" 2>&1
system_profiler SPDisplaysDataType > "$PROOF_DIR/graphics-info.txt" 2>&1 || true
- name: Generate desktop-c project
run: |
mkdir -p "$PROOF_DIR"
./gradlew "$GENERATE_TASK" --stacktrace 2>&1 | tee "$PROOF_DIR/generate.log"
exit "${PIPESTATUS[0]}"
- name: Validate pinned macOS static dependencies
if: ${{ matrix.example.id == 'basic' }}
run: |
set -euo pipefail
SOURCE_DIR="$GITHUB_WORKSPACE/examples/${{ matrix.example.id }}/platforms/desktop/teavm-c/builder/build/dist"
BUILD_DIR="$SOURCE_DIR/build/cmake-pinned-dependencies"
cmake -S "$SOURCE_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug 2>&1 | tee "$PROOF_DIR/pinned-configure.log"
cmake --build "$BUILD_DIR" --target glfw gdx_teavm_glew --parallel 3 2>&1 | tee "$PROOF_DIR/pinned-build.log"
GLFW_ARCHIVE="$(find "$BUILD_DIR" -type f -name 'libglfw3.a' -print -quit)"
GLEW_ARCHIVE="$(find "$BUILD_DIR" -type f -name 'libgdx_teavm_glew.a' -print -quit)"
test -n "$GLFW_ARCHIVE"
test -n "$GLEW_ARCHIVE"
file "$GLFW_ARCHIVE" "$GLEW_ARCHIVE" | tee "$PROOF_DIR/pinned-archives.txt"
- name: Build desktop-c debug executable for runtime proof
run: |
set -euo pipefail
SOURCE_DIR="$GITHUB_WORKSPACE/examples/${{ matrix.example.id }}/platforms/desktop/teavm-c/builder/build/dist"
BUILD_DIR="$SOURCE_DIR/build/cmake-runtime"
GLFW_SRC="$RUNNER_TEMP/glfw"
cmake -S "$SOURCE_DIR" -B "$BUILD_DIR" \
-DCMAKE_BUILD_TYPE=Debug \
-DFETCHCONTENT_SOURCE_DIR_GDX_TEAVM_GLFW="$GLFW_SRC" 2>&1 | tee "$PROOF_DIR/build-configure.log"
cmake --build "$BUILD_DIR" --config Debug --parallel 3 2>&1 | tee "$PROOF_DIR/build.log"
APP_ABS="$GITHUB_WORKSPACE/$APP_PATH"
otool -L "$APP_ABS" | tee "$PROOF_DIR/native-dependencies.txt"
if grep -Eiq 'lib(glfw|glew)' "$PROOF_DIR/native-dependencies.txt"; then
echo "The macOS executable still depends on a GLFW or GLEW shared library."
exit 1
fi
- name: Run and capture proof
run: |
set -euo pipefail
mkdir -p "$PROOF_DIR"
emit_github_error() {
local title="$1"
local message
message="$(
{
echo "$title"
cat "$PROOF_DIR/app.log" "$PROOF_DIR/lldb.log" "$PROOF_DIR/screencapture.log" 2>/dev/null || true
} | tail -n 80 | tr '\r\n' ' ' | head -c 3500
)"
message="${message//'%'/'%25'}"
echo "::error title=$title::$message"
}
cleanup() {
if [ -n "${APP_PID:-}" ] && kill -0 "$APP_PID" 2>/dev/null; then
pkill -P "$APP_PID" 2>/dev/null || true
kill "$APP_PID" 2>/dev/null || true
wait "$APP_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT
APP_ABS="$GITHUB_WORKSPACE/$APP_PATH"
APP_DIR="$(dirname "$APP_ABS")"
APP_NAME="$(basename "$APP_ABS")"
chmod +x "$APP_ABS"
pushd "$APP_DIR"
"./$APP_NAME" > "$GITHUB_WORKSPACE/$PROOF_DIR/app.log" 2>&1 &
APP_PID=$!
popd
sleep 12
if ! kill -0 "$APP_PID" 2>/dev/null; then
echo "${{ matrix.example.label }} desktop-c process exited before proof capture."
cat "$PROOF_DIR/app.log" || true
pushd "$APP_DIR"
lldb --batch -o run -o "bt all" -- "./$APP_NAME" > "$GITHUB_WORKSPACE/$PROOF_DIR/lldb.log" 2>&1 || true
popd
emit_github_error "${{ matrix.example.label }} desktop-c macOS app exited before proof"
exit 1
fi
if ! screencapture -x "$PROOF_DIR/screenshot.png" 2> "$PROOF_DIR/screencapture.log"; then
emit_github_error "${{ matrix.example.label }} desktop-c macOS screencapture failed"
exit 1
fi
if [ ! -s "$PROOF_DIR/screenshot.png" ]; then
emit_github_error "${{ matrix.example.label }} desktop-c macOS screenshot was empty"
exit 1
fi
- name: Collect FPS log
if: always()
run: |
FPS_SOURCE="$(dirname "$GITHUB_WORKSPACE/$APP_PATH")/fps.log"
if [ -s "$FPS_SOURCE" ]; then
cp "$FPS_SOURCE" "$PROOF_DIR/fps.log"
cat "$PROOF_DIR/fps.log"
else
echo "::error title=Missing FPS log::${{ matrix.example.label }} desktop-c macOS did not write fps.log"
exit 1
fi
- name: Upload runtime proof
uses: actions/upload-artifact@v7
if: always()
with:
name: ${{ matrix.example.id }}-desktop-c-macos-runtime-proof
path: runtime-proof
if-no-files-found: warn
overwrite: true
desktop-c-windows:
name: ${{ matrix.example.label }} desktop-c Windows runtime
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: windows-2022
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
example:
- id: basic
label: Basic
executable: app
- id: freetype
label: FreeType
executable: freetype
- id: controllers
label: Controllers
executable: controllers
env:
APP_PATH: examples/${{ matrix.example.id }}/platforms/desktop/teavm-c/builder/build/dist/c/release/${{ matrix.example.executable }}_debug.exe
BUILD_TASK: :examples:${{ matrix.example.id }}:platforms:desktop:teavm-c:builder:${{ matrix.example.id }}_desktop_c_debug_build
PROOF_DIR: runtime-proof/windows/${{ matrix.example.id }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
- name: Record runner info
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path $env:PROOF_DIR | Out-Null
@(
[System.Environment]::OSVersion.VersionString
"Architecture: $env:PROCESSOR_ARCHITECTURE"
(cmake --version | Select-Object -First 1)
) | Set-Content -Path "$env:PROOF_DIR/system-info.txt"
- name: Build desktop-c debug executable
shell: pwsh
run: |
& .\gradlew.bat $env:BUILD_TASK --stacktrace 2>&1 |
Tee-Object -FilePath "$env:PROOF_DIR/build.log"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
- name: Verify GLFW and GLEW are statically linked
shell: pwsh
run: |
$app = Join-Path $env:GITHUB_WORKSPACE $env:APP_PATH
if (-not (Test-Path -LiteralPath $app -PathType Leaf)) {
throw "Desktop-C executable was not built at $app"
}
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
$dumpbin = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -find 'VC\Tools\MSVC\**\bin\Hostx64\x64\dumpbin.exe' |
Select-Object -First 1
if (-not $dumpbin) {
throw 'Could not locate dumpbin.exe'
}
$dependencies = & $dumpbin /DEPENDENTS $app 2>&1
$dependencies | Tee-Object -FilePath "$env:PROOF_DIR/native-dependencies.txt"
if (($dependencies -join "`n") -match '(?i)(glfw|glew).*\.dll') {
throw 'The Windows executable still depends on a GLFW or GLEW DLL.'
}
- name: Install Mesa software OpenGL
id: msys2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: mingw-w64-x86_64-mesa
- name: Deploy Mesa software OpenGL
shell: pwsh
env:
MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }}
run: |
$app = Join-Path $env:GITHUB_WORKSPACE $env:APP_PATH
$appDir = Split-Path -Parent $app
$mesaBin = Join-Path $env:MSYS2_LOCATION 'mingw64\bin'
$mesaFiles = @(
(Join-Path $mesaBin 'opengl32.dll')
(Join-Path $mesaBin 'libgallium_wgl.dll')
)
foreach ($mesaFile in $mesaFiles) {
if (-not (Test-Path -LiteralPath $mesaFile -PathType Leaf)) {
throw "Mesa runtime file was not installed at $mesaFile"
}
Copy-Item -LiteralPath $mesaFile -Destination $appDir -Force
}
$mesaBin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
Get-FileHash -Algorithm SHA256 -LiteralPath $mesaFiles |
Format-Table -AutoSize |
Out-String |
Set-Content -Path "$env:PROOF_DIR/mesa-files.txt"
- name: Run and capture proof
shell: pwsh
run: |
$app = Join-Path $env:GITHUB_WORKSPACE $env:APP_PATH
$appDir = Split-Path -Parent $app
$proofDir = Join-Path $env:GITHUB_WORKSPACE $env:PROOF_DIR
$stdout = Join-Path $proofDir 'app.log'
$stderr = Join-Path $proofDir 'app-error.log'
$env:GALLIUM_DRIVER = 'llvmpipe'
$process = Start-Process -FilePath $app -WorkingDirectory $appDir -RedirectStandardOutput $stdout -RedirectStandardError $stderr -PassThru
try {
Start-Sleep -Seconds 10
$process.Refresh()
if ($process.HasExited) {
Get-Content -LiteralPath $stdout, $stderr -ErrorAction SilentlyContinue
throw '${{ matrix.example.label }} desktop-c process exited before proof capture.'
}
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
try {
$graphics.CopyFromScreen($bounds.Left, $bounds.Top, 0, 0, $bounds.Size)
$screenshot = Join-Path $proofDir 'screenshot.png'
$bitmap.Save($screenshot, [System.Drawing.Imaging.ImageFormat]::Png)
}
finally {
$graphics.Dispose()
$bitmap.Dispose()
}
if (-not (Test-Path -LiteralPath $screenshot) -or
(Get-Item -LiteralPath $screenshot).Length -eq 0) {
throw '${{ matrix.example.label }} desktop-c Windows screenshot was empty.'
}
}
finally {
if (-not $process.HasExited) {
Stop-Process -Id $process.Id -Force
$process.WaitForExit()
}
}
- name: Collect FPS log
if: always()
shell: pwsh
run: |
$source = Join-Path (Split-Path -Parent (Join-Path $env:GITHUB_WORKSPACE $env:APP_PATH)) 'fps.log'
$destination = Join-Path (Join-Path $env:GITHUB_WORKSPACE $env:PROOF_DIR) 'fps.log'
if ((Test-Path -LiteralPath $source -PathType Leaf) -and (Get-Item -LiteralPath $source).Length -gt 0) {
Copy-Item -LiteralPath $source -Destination $destination -Force
Get-Content -LiteralPath $destination
}
else {
throw '${{ matrix.example.label }} desktop-c Windows did not write fps.log'
}
- name: Upload runtime proof
uses: actions/upload-artifact@v7
if: always()
with:
name: ${{ matrix.example.id }}-desktop-c-windows-runtime-proof
path: runtime-proof
if-no-files-found: warn
overwrite: true
android:
name: ${{ matrix.example.label }} backend-android runtime
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
example:
- id: basic
label: Basic
application_id: com.github.xpenatan.gdx.teavm.examples.basic.android
activity: com.github.xpenatan.gdx.teavm.examples.basic.android.MainActivity
- id: freetype
label: FreeType
application_id: com.github.xpenatan.gdx.teavm.examples.freetype.android
activity: com.github.xpenatan.gdx.teavm.examples.freetype.android.MainActivity
- id: controllers
label: Controllers
application_id: com.github.xpenatan.gdx.teavm.examples.controllers.android
activity: com.github.xpenatan.gdx.teavm.examples.controllers.android.MainActivity
env:
APK_PATH: examples/${{ matrix.example.id }}/platforms/android/build/outputs/apk/debug/android-x86_64-debug.apk
APP_ID: ${{ matrix.example.application_id }}
ACTIVITY: ${{ matrix.example.activity }}
BUILD_TASK: :examples:${{ matrix.example.id }}:platforms:android:assembleDebug
EXAMPLE_LABEL: ${{ matrix.example.label }}
PROOF_DIR: runtime-proof/android/${{ matrix.example.id }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
- name: Set up Android SDK
uses: android-actions/setup-android@v4
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Install Android native toolchain
run: sdkmanager "platforms;android-36" "ndk;27.0.12077973" "cmake;3.22.1"
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Build backend-android debug APKs
run: |
set -o pipefail
mkdir -p "$PROOF_DIR"
./gradlew "$BUILD_TASK" --console=plain --stacktrace 2>&1 | tee "$PROOF_DIR/build.log"
if [ ! -s "$APK_PATH" ]; then
echo "Expected x86_64 APK was not built at $APK_PATH"
exit 1
fi
- name: Run Android emulator and capture proof
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
arch: x86_64
target: google_apis
profile: pixel_6
disable-animations: true
emulator-options: -no-snapshot -no-window -no-audio -gpu swiftshader_indirect -no-boot-anim
script: bash .github/scripts/android-runtime-proof.sh
- name: Upload runtime proof
uses: actions/upload-artifact@v7
if: always()
with:
name: ${{ matrix.example.id }}-android-runtime-proof
path: runtime-proof
if-no-files-found: warn
overwrite: true
ios-simulator:
name: ${{ matrix.example.label }} backend-ios simulator runtime
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: macos-15
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
example:
- id: basic
label: Basic
bundle_id: com.github.xpenatan.gdxteavm.basic.ios
- id: freetype
label: FreeType
bundle_id: com.github.xpenatan.gdxteavm.freetype.ios
- id: controllers
label: Controllers
bundle_id: com.github.xpenatan.gdxteavm.controllers.ios
env:
APP_PATH: examples/${{ matrix.example.id }}/platforms/ios/build/xcode-derived/ios/Build/Products/Debug-iphonesimulator/GdxTeaVMIOSSpike.app
BUILD_TASK: :examples:${{ matrix.example.id }}:platforms:ios:gdx_teavm_ios_build_simulator
BUNDLE_ID: ${{ matrix.example.bundle_id }}
EXAMPLE_DIR: examples/${{ matrix.example.id }}/platforms/ios
PROOF_DIR: runtime-proof/ios/${{ matrix.example.id }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Record runner and simulator info
run: |
set -euo pipefail
mkdir -p "$PROOF_DIR"
{
sw_vers
uname -a
xcode-select -p
xcodebuild -version
xcrun simctl list runtimes available
xcrun simctl list devices available
} > "$PROOF_DIR/system-info.txt" 2>&1
xcrun simctl list devices available -j > "$PROOF_DIR/devices.json"
- name: Build backend-ios simulator app
run: |
set -uo pipefail
mkdir -p "$PROOF_DIR"
emit_github_error() {
local title="$1"
local message
set +e
message="$(
{
echo "$title"
if [ -f "$PROOF_DIR/xcodebuild.log" ]; then
echo "--- xcodebuild.log errors ---"
grep -Ei "error:|fatal error|undefined symbols|ld:|clang:|swift|provision|signing|failed|BUILD FAILED" "$PROOF_DIR/xcodebuild.log" | tail -n 160 || true
echo "--- xcodebuild.log tail ---"
tail -n 120 "$PROOF_DIR/xcodebuild.log"
fi
echo "--- gradle build.log errors ---"
grep -Ei "error:|fatal error|undefined symbols|ld:|clang:|swift|xcodebuild|failed|BUILD FAILED|Command failed" "$PROOF_DIR/build.log" | tail -n 160 || true
echo "--- gradle build.log tail ---"
tail -n 220 "$PROOF_DIR/build.log" 2>/dev/null || true
} | tail -n 260 | tr '\r\n' ' ' | tail -c 12000
)"
set -e
message="${message//'%'/'%25'}"
echo "::error title=$title::$message"
}
emit_text_error() {
local title="$1"
local message="$2"
message="$(printf '%s' "$message" | tr '\r\n' ' ' | tail -c 3500)"
message="${message//'%'/'%25'}"
echo "::error title=$title::$message"
}
emit_file_tail_error() {
local title="$1"
local file="$2"
if [ -f "$file" ]; then
emit_text_error "$title" "$(tail -n 80 "$file" 2>&1)"
else
emit_text_error "$title" "Missing expected log file: $file"
fi
}
set +e
./gradlew "$BUILD_TASK" --console=plain 2>&1 | tee "$PROOF_DIR/build.log"
status="${PIPESTATUS[0]}"
set -e
if [ "$status" -ne 0 ]; then
XCODE_PROJECT="$GITHUB_WORKSPACE/$EXAMPLE_DIR/build/dist/ios/xcode/GdxTeaVMIOSSpike.xcodeproj"
if [ -d "$XCODE_PROJECT" ]; then
xcodebuild \
-project "$XCODE_PROJECT" \
-scheme GdxTeaVMIOSSpike \
-sdk iphonesimulator \
-configuration Debug \
-derivedDataPath "$GITHUB_WORKSPACE/$EXAMPLE_DIR/build/xcode-derived/ios" \
CODE_SIGNING_ALLOWED=NO \
build > "$PROOF_DIR/xcodebuild.log" 2>&1 || true
fi
emit_text_error "backend-ios generated Xcode project" "$(find "$GITHUB_WORKSPACE/$EXAMPLE_DIR/build/dist/ios" -maxdepth 3 -print 2>&1 | tail -n 80)"
emit_file_tail_error "backend-ios direct xcodebuild tail" "$PROOF_DIR/xcodebuild.log"
emit_file_tail_error "backend-ios Gradle build tail" "$PROOF_DIR/build.log"
emit_github_error "backend-ios simulator build failed"
exit "$status"
fi
- name: Run iOS simulator and capture proof
timeout-minutes: 12
run: |
set -euo pipefail
mkdir -p "$PROOF_DIR"
emit_github_error() {
local title="$1"
local message
set +e
message="$(
{
echo "$title"
cat \
"$PROOF_DIR/simulator.log" \
"$PROOF_DIR/launch.log" \
"$PROOF_DIR/app.log" \
"$PROOF_DIR/processes.txt" \
"$PROOF_DIR/screenshot.log" \
"$PROOF_DIR/video.log" \
2>/dev/null || true
} | tail -n 160 | tr '\r\n' ' ' | tail -c 8000
)"
set -e
message="${message//'%'/'%25'}"
echo "::error title=$title::$message"
}
trap 'emit_github_error "backend-ios simulator proof failed at line $LINENO"' ERR
select_simulator() {
python3 - "$PROOF_DIR/devices.json" "$PROOF_DIR/selected-device.txt" <<'PY'
import json
import re
import sys
devices_path = sys.argv[1]
selected_path = sys.argv[2]
data = json.load(open(devices_path, encoding="utf-8"))
choices = []
for runtime, devices in data.get("devices", {}).items():
if "iOS" not in runtime:
continue
version = tuple(int(part) for part in re.findall(r"\d+", runtime))
for device in devices:
name = device.get("name", "")
if not device.get("isAvailable") or "iPhone" not in name:
continue
score = 0
for index, prefix in enumerate(("iPhone 16", "iPhone 15", "iPhone 14", "iPhone 13", "iPhone 12")):
if name.startswith(prefix):
score = 100 - index
break
if "Pro Max" in name:
score -= 10
choices.append((version, score, runtime, name, device["udid"]))
if not choices:
raise SystemExit("No available iPhone simulator device found")
choices.sort(reverse=True)
version, score, runtime, name, udid = choices[0]
with open(selected_path, "w", encoding="utf-8") as output:
output.write(f"{name} ({runtime}) {udid}\n")
print(udid)
PY
}
process_is_running() {
kill -0 "$1" 2>/dev/null
}
stop_background_process() {
local pid="${1:-}"
local signal="${2:-TERM}"
local label="${3:-background process}"
if [ -z "$pid" ]; then
return 0
fi
if ! process_is_running "$pid"; then
wait "$pid" 2>/dev/null || true
return 0
fi
kill "-$signal" "$pid" 2>/dev/null || true
for _ in {1..10}; do
if ! process_is_running "$pid"; then
wait "$pid" 2>/dev/null || true
return 0
fi
sleep 1
done
echo "$label did not exit after SIG$signal; forcing SIGKILL." >> "$PROOF_DIR/simulator.log"
kill -KILL "$pid" 2>/dev/null || true
for _ in {1..5}; do
if ! process_is_running "$pid"; then
wait "$pid" 2>/dev/null || true
return 0
fi
sleep 1
done
echo "$label remained alive after SIGKILL; the step timeout will stop it." >> "$PROOF_DIR/simulator.log"
}
run_bounded_command() {
local limit_seconds="$1"
local label="$2"
shift 2
"$@" &
local pid=$!
local elapsed=0
while process_is_running "$pid" && [ "$elapsed" -lt "$limit_seconds" ]; do
sleep 1
elapsed=$((elapsed + 1))
done
if process_is_running "$pid"; then
echo "$label exceeded ${limit_seconds}s." >> "$PROOF_DIR/simulator.log"
stop_background_process "$pid" TERM "$label"
return 124
fi
local status=0
wait "$pid" || status=$?
return "$status"
}
cleanup() {
stop_background_process "${VIDEO_PID:-}" INT "simulator video capture"
stop_background_process "${LOG_PID:-}" TERM "simulator log stream"
stop_background_process "${APP_PID:-}" TERM "simulator app console"
if [ -n "${DEVICE_UDID:-}" ]; then
run_bounded_command 20 "simulator app termination" \
xcrun simctl terminate "$DEVICE_UDID" "$BUNDLE_ID" >> "$PROOF_DIR/simulator.log" 2>&1 || true
run_bounded_command 30 "simulator shutdown" \
xcrun simctl shutdown "$DEVICE_UDID" >> "$PROOF_DIR/simulator.log" 2>&1 || true
fi
}
trap cleanup EXIT
APP_ABS="$GITHUB_WORKSPACE/$APP_PATH"
if [ ! -d "$APP_ABS" ]; then
echo "iOS simulator app was not built at $APP_ABS" > "$PROOF_DIR/simulator.log"
emit_github_error "backend-ios simulator app bundle missing"
exit 1
fi
DEVICE_UDID="$(select_simulator)"
echo "Selected simulator: $(cat "$PROOF_DIR/selected-device.txt")" | tee -a "$PROOF_DIR/simulator.log"
echo "Booting iOS simulator..."
run_bounded_command 20 "pre-run simulator shutdown" \
xcrun simctl shutdown "$DEVICE_UDID" >> "$PROOF_DIR/simulator.log" 2>&1 || true
run_bounded_command 30 "simulator boot request" \
xcrun simctl boot "$DEVICE_UDID" >> "$PROOF_DIR/simulator.log" 2>&1 || true
run_bounded_command 120 "simulator boot" \
xcrun simctl bootstatus "$DEVICE_UDID" -b >> "$PROOF_DIR/simulator.log" 2>&1
run_bounded_command 20 "Simulator application launch" \
open -a Simulator --args -CurrentDeviceUDID "$DEVICE_UDID" >> "$PROOF_DIR/simulator.log" 2>&1 || true
echo "iOS simulator booted."
run_bounded_command 20 "pre-run app termination" \
xcrun simctl terminate "$DEVICE_UDID" "$BUNDLE_ID" >> "$PROOF_DIR/simulator.log" 2>&1 || true
run_bounded_command 20 "simulator app uninstall" \
xcrun simctl uninstall "$DEVICE_UDID" "$BUNDLE_ID" >> "$PROOF_DIR/simulator.log" 2>&1 || true
run_bounded_command 60 "simulator app install" \
xcrun simctl install "$DEVICE_UDID" "$APP_ABS" >> "$PROOF_DIR/simulator.log" 2>&1
xcrun simctl spawn "$DEVICE_UDID" log stream --style compact --level debug \
--predicate "process == 'GdxTeaVMIOSSpike'" > "$PROOF_DIR/app.log" 2>&1 &
LOG_PID=$!
xcrun simctl launch --console-pty "$DEVICE_UDID" "$BUNDLE_ID" > "$PROOF_DIR/launch.log" 2>&1 &
APP_PID=$!
echo "iOS app launched; collecting runtime samples..."
sleep 12
run_bounded_command 20 "simulator process listing" \
xcrun simctl spawn "$DEVICE_UDID" ps -A > "$PROOF_DIR/processes.txt" 2>&1 || true
if ! grep -q "GdxTeaVMIOSSpike" "$PROOF_DIR/processes.txt"; then
echo "GdxTeaVMIOSSpike was not visible in simulator ps output; continuing to screenshot proof." >> "$PROOF_DIR/simulator.log"
fi
echo "Capturing iOS runtime screenshot..."
if ! run_bounded_command 60 "simulator screenshot" \
xcrun simctl io "$DEVICE_UDID" screenshot "$PROOF_DIR/screenshot.png" > "$PROOF_DIR/screenshot.log" 2>&1; then
echo "Initial simulator screenshot attempt failed; retrying once." | tee -a "$PROOF_DIR/screenshot.log"
rm -f "$PROOF_DIR/screenshot.png"
sleep 3
if ! run_bounded_command 60 "simulator screenshot retry" \
xcrun simctl io "$DEVICE_UDID" screenshot "$PROOF_DIR/screenshot.png" >> "$PROOF_DIR/screenshot.log" 2>&1; then
emit_github_error "backend-ios simulator screenshot failed"
exit 1
fi
fi
if [ ! -s "$PROOF_DIR/screenshot.png" ]; then
emit_github_error "backend-ios simulator screenshot was empty"
exit 1
fi
echo "Recording iOS runtime proof..."
xcrun simctl io "$DEVICE_UDID" recordVideo "$PROOF_DIR/video.mp4" > "$PROOF_DIR/video.log" 2>&1 &
VIDEO_PID=$!
sleep 6
echo "Stopping iOS capture processes..."
stop_background_process "$VIDEO_PID" INT "simulator video capture"
VIDEO_PID=""
stop_background_process "$LOG_PID" TERM "simulator log stream"
LOG_PID=""
stop_background_process "$APP_PID" TERM "simulator app console"
APP_PID=""
DATA_CONTAINER="$(run_bounded_command 20 "simulator data-container lookup" \
xcrun simctl get_app_container "$DEVICE_UDID" "$BUNDLE_ID" data 2>> "$PROOF_DIR/simulator.log" || true)"
FPS_SOURCE=""
if [ -n "$DATA_CONTAINER" ] && [ -d "$DATA_CONTAINER" ]; then
FPS_SOURCE="$(find "$DATA_CONTAINER" -type f -name fps.log -print -quit 2>/dev/null || true)"
fi
if [ -n "$FPS_SOURCE" ] && [ -s "$FPS_SOURCE" ]; then
cp "$FPS_SOURCE" "$PROOF_DIR/fps.log"
else
grep -hE 'FPSLogger.*fps:[[:space:]]*[0-9]+' "$PROOF_DIR/launch.log" > "$PROOF_DIR/fps.log" || true
if [ ! -s "$PROOF_DIR/fps.log" ]; then
grep -hE 'FPSLogger.*fps:[[:space:]]*[0-9]+' "$PROOF_DIR/app.log" > "$PROOF_DIR/fps.log" || true
fi
fi
if [ ! -s "$PROOF_DIR/fps.log" ]; then
echo "::error title=Missing FPS log::${{ matrix.example.label }} backend-ios did not produce FPSLogger output"
exit 1
fi
cat "$PROOF_DIR/fps.log"
- name: Upload runtime proof
uses: actions/upload-artifact@v7
if: always()
with:
name: ${{ matrix.example.id }}-ios-simulator-runtime-proof
path: runtime-proof
if-no-files-found: warn
overwrite: true
merge-runtime-proofs:
name: Merge runtime proofs
if: ${{ always() && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success') }}
needs:
- desktop-c-linux
- desktop-c-macos
- desktop-c-windows
- android
- ios-simulator
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout FPS summary script
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Restore previous final proof
if: ${{ github.run_attempt != '1' }}
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: runtime-examples-proof
path: runtime-proof
- name: Download runtime proofs
uses: actions/download-artifact@v8
with:
pattern: '*-runtime-proof'
path: runtime-proof
merge-multiple: true
- name: Create FPS summary
run: |
python3 .github/scripts/runtime-fps-summary.py runtime-proof
cat runtime-proof/fps-summary.md
cat runtime-proof/fps-summary.md >> "$GITHUB_STEP_SUMMARY"
- name: Upload final runtime proof
uses: actions/upload-artifact@v7
with:
name: runtime-examples-proof
path: runtime-proof
if-no-files-found: error
overwrite: true
- name: Delete temporary runtime proof artifacts
uses: actions/github-script@v9
with:
script: |
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner: context.repo.owner,
repo: context.repo.repo,
run_id: Number(process.env.GITHUB_RUN_ID),
per_page: 100,
});
const temporaryArtifacts = artifacts.filter((artifact) =>
artifact.name.endsWith('-runtime-proof')
);
for (const artifact of temporaryArtifacts) {
await github.rest.actions.deleteArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: artifact.id,
});
core.info(`Deleted temporary artifact ${artifact.name} (${artifact.id})`);
}
- name: Require complete FPS coverage
run: python3 .github/scripts/runtime-fps-summary.py runtime-proof --require-complete