Runtime Examples #88
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |