fix(psp): cursor-based desktop UX, browser windowed rendering, kiosk … #102
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: Main CI | |
| on: | |
| push: | |
| branches: [main] | |
| tags: | |
| - 'v*' | |
| workflow_dispatch: | |
| inputs: | |
| create_release: | |
| description: 'Create a release (even without tag)' | |
| required: false | |
| type: boolean | |
| default: false | |
| run_benchmarks: | |
| description: 'Run criterion benchmarks' | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| issues: write | |
| pages: write | |
| id-token: write | |
| concurrency: | |
| group: main-ci-${{ github.sha }} | |
| cancel-in-progress: false | |
| env: | |
| DOCKER_BUILDKIT: 1 | |
| COMPOSE_DOCKER_CLI_BUILD: 1 | |
| jobs: | |
| ci: | |
| name: CI Pipeline | |
| runs-on: self-hosted | |
| timeout-minutes: 60 | |
| steps: | |
| # -- Cleanup & Setup --------------------------------------------------- | |
| - name: Pre-checkout cleanup | |
| run: | | |
| for item in outputs target psp_output_file.log .git/index.lock; do | |
| if [ -d "$item" ] || [ -f "$item" ]; then | |
| docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \ | |
| "rm -rf /workspace/$item" 2>/dev/null || \ | |
| sudo rm -rf "$item" 2>/dev/null || true | |
| fi | |
| done | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| clean: true | |
| - name: Set UID/GID | |
| run: | | |
| echo "USER_ID=$(id -u)" >> $GITHUB_ENV | |
| echo "GROUP_ID=$(id -g)" >> $GITHUB_ENV | |
| - name: Build CI Docker image | |
| run: docker compose --profile ci build rust-ci | |
| # -- Format Check ------------------------------------------------------ | |
| - name: Format check | |
| timeout-minutes: 5 | |
| run: docker compose --profile ci run --rm rust-ci cargo fmt --all -- --check | |
| # -- Clippy Lint ------------------------------------------------------- | |
| - name: Clippy | |
| timeout-minutes: 10 | |
| run: docker compose --profile ci run --rm rust-ci cargo clippy --workspace -- -D warnings | |
| # -- Nightly Clippy (advisory) ----------------------------------------- | |
| - name: Clippy (nightly, advisory) | |
| timeout-minutes: 10 | |
| continue-on-error: true | |
| run: > | |
| docker compose --profile ci run --rm rust-ci | |
| cargo +nightly clippy --workspace -- -D warnings 2>&1 || true | |
| # -- Documentation Build ----------------------------------------------- | |
| - name: Doc build | |
| timeout-minutes: 10 | |
| run: > | |
| docker compose --profile ci run --rm | |
| -e RUSTDOCFLAGS="-D warnings" | |
| rust-ci cargo doc --workspace --no-deps | |
| # -- Markdown Link Check ----------------------------------------------- | |
| - name: Markdown link check | |
| timeout-minutes: 5 | |
| run: | | |
| FAIL=0 | |
| for target in *.md docs/ scripts/psp-scenarios.md; do | |
| [ -e "$target" ] || continue | |
| md-link-checker "$target" --internal-only || FAIL=1 | |
| done | |
| exit $FAIL | |
| # -- Tests ------------------------------------------------------------- | |
| - name: Test | |
| timeout-minutes: 15 | |
| run: | | |
| set -o pipefail | |
| docker compose --profile ci run --rm rust-ci \ | |
| cargo test --workspace 2>&1 | tee test_output.txt | |
| - name: Test metrics summary | |
| if: always() | |
| run: | | |
| if [ ! -f test_output.txt ]; then | |
| echo "No test output captured (test step may have been skipped)." | |
| echo "### Test Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "No test output available." >> $GITHUB_STEP_SUMMARY | |
| exit 0 | |
| fi | |
| PASS=$(grep -c '\.\.\. *ok$' test_output.txt 2>/dev/null) || PASS=0 | |
| FAIL=$(grep -c 'FAILED$' test_output.txt 2>/dev/null) || FAIL=0 | |
| TOTAL=$((PASS + FAIL)) | |
| echo "### Test Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "| Metric | Count |" >> $GITHUB_STEP_SUMMARY | |
| echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Passed | $PASS |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Failed | $FAIL |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Total | $TOTAL |" >> $GITHUB_STEP_SUMMARY | |
| # -- Release Build ----------------------------------------------------- | |
| - name: Build (release) | |
| timeout-minutes: 15 | |
| run: docker compose --profile ci run --rm rust-ci cargo build --workspace --release | |
| # -- Screenshot Regression --------------------------------------------- | |
| - name: Screenshot tests (generate) | |
| timeout-minutes: 5 | |
| run: | | |
| docker compose --profile ci run --rm \ | |
| -e SDL_VIDEODRIVER=dummy -e SDL_RENDER_DRIVER=software \ | |
| -e OASIS_FIXED_TIME="2025-06-15 12:00:00" \ | |
| -e OASIS_FIXED_FRAME=0 \ | |
| rust-ci cargo run -p oasis-app --bin screenshot-tests --release | |
| - name: Screenshot regression comparison | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| if [ -d screenshots/baseline ]; then | |
| bash scripts/compare-screenshots.sh \ | |
| --baseline screenshots/baseline \ | |
| --actual screenshots/tests \ | |
| --threshold 0.1 | |
| else | |
| echo "No baselines found -- skipping regression comparison." | |
| echo "To generate baselines, run locally:" | |
| echo " OASIS_FIXED_TIME='2025-06-15 12:00:00' OASIS_FIXED_FRAME=0 \\" | |
| echo " cargo run -p oasis-app --bin screenshot-tests --release -- --bless" | |
| echo " cp -r screenshots/tests/ screenshots/baseline/" | |
| echo "Then commit screenshots/baseline/ to the repository." | |
| fi | |
| - name: Upload screenshot report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: screenshot-report | |
| path: screenshots/tests/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| # -- cargo-deny -------------------------------------------------------- | |
| - name: cargo-deny | |
| run: docker compose --profile ci run --rm rust-ci cargo deny check | |
| # -- Benchmarks (manual) ----------------------------------------------- | |
| - name: Benchmarks | |
| if: inputs.run_benchmarks == true | |
| run: | | |
| set -o pipefail | |
| docker compose --profile ci run --rm rust-ci \ | |
| cargo bench --workspace 2>&1 | tee bench_results.txt | |
| echo "::group::Benchmark Results" | |
| grep -E '(time:|bench)' bench_results.txt || true | |
| echo "::endgroup::" | |
| - name: Upload benchmark results | |
| if: inputs.run_benchmarks == true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: benchmark-results | |
| path: bench_results.txt | |
| if-no-files-found: ignore | |
| retention-days: 30 | |
| - name: Upload criterion baseline | |
| if: inputs.run_benchmarks == true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: criterion-baseline | |
| path: target/criterion/ | |
| if-no-files-found: ignore | |
| retention-days: 90 | |
| # -- Coverage ---------------------------------------------------------- | |
| - name: Coverage (cargo-llvm-cov) | |
| continue-on-error: true | |
| run: | | |
| docker compose --profile ci run --rm rust-ci bash -c " | |
| rustup component add llvm-tools-preview 2>/dev/null || true | |
| cargo install cargo-llvm-cov --locked 2>/dev/null || true | |
| if command -v cargo-llvm-cov &>/dev/null; then | |
| cargo llvm-cov --workspace --no-fail-fast \ | |
| --ignore-filename-regex '(tests?\.rs|benches?\.rs|main\.rs)' \ | |
| --fail-under-lines 70 \ | |
| 2>&1 | tee /app/coverage_output.txt | |
| else | |
| echo 'cargo-llvm-cov not available, skipping coverage' | |
| fi | |
| " | |
| - name: Coverage summary | |
| if: always() | |
| run: | | |
| if [ ! -f coverage_output.txt ]; then | |
| echo "### Coverage" >> $GITHUB_STEP_SUMMARY | |
| echo "Coverage step was skipped or unavailable." >> $GITHUB_STEP_SUMMARY | |
| exit 0 | |
| fi | |
| echo "### Coverage" >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| tail -20 coverage_output.txt >> $GITHUB_STEP_SUMMARY | |
| echo '```' >> $GITHUB_STEP_SUMMARY | |
| # -- PSP Backend Build ------------------------------------------------- | |
| - name: Setup PSP SDK | |
| uses: ./.github/actions/setup-psp | |
| - name: Build PSP EBOOT | |
| run: | | |
| cd crates/oasis-backend-psp | |
| RUST_PSP_BUILD_STD=1 cargo +nightly psp --release | |
| - name: Upload PSP EBOOT artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: psp-eboot | |
| path: crates/oasis-backend-psp/target/mipsel-sony-psp-std/release/EBOOT.PBP | |
| if-no-files-found: ignore | |
| retention-days: 90 | |
| # -- PSP Emulator Test ------------------------------------------------- | |
| - name: Run OASIS_OS EBOOT in PPSSPPHeadless | |
| run: | | |
| docker compose --profile psp run --rm -e PPSSPP_HEADLESS=1 ppsspp \ | |
| /roms/release/EBOOT.PBP --timeout=30 2>/dev/null || true | |
| if [ -f psp_output_file.log ]; then | |
| cat psp_output_file.log | |
| else | |
| echo "No output log -- headless exited without crash (TIMEOUT ok)" | |
| fi | |
| # -- Cleanup ----------------------------------------------------------- | |
| - name: Fix Docker file ownership | |
| if: always() | |
| run: | | |
| for dir in target outputs; do | |
| if [ -d "$dir" ]; then | |
| docker run --rm -v "$(pwd)/$dir:/workspace" busybox:1.36.1 \ | |
| chown -Rh "$(id -u):$(id -g)" /workspace 2>/dev/null || true | |
| fi | |
| done | |
| # -- Summary ----------------------------------------------------------- | |
| - name: CI Summary | |
| if: always() | |
| run: | | |
| echo "## Main CI Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY | |
| echo "**Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY | |
| - name: Create issue on failure | |
| if: failure() && github.ref == 'refs/heads/main' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh issue create \ | |
| --title "Main CI Failed: $(date +%Y-%m-%d)" \ | |
| --body "$(cat <<'ISSUE_EOF' | |
| ## CI Failure Report | |
| **Commit**: ${{ github.sha }} | |
| **Run**: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| Please investigate and fix the issues. | |
| ISSUE_EOF | |
| )" \ | |
| --label "ci-failure,automated" || echo "::warning::Could not create failure issue" | |
| # -- Build Release Binaries (separate job, depends on CI) ---------------- | |
| build-release-binaries: | |
| name: Build Release Binaries | |
| needs: ci | |
| if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.create_release == 'true' | |
| runs-on: self-hosted | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Pre-checkout cleanup | |
| run: | | |
| for item in outputs release-binaries .git/index.lock; do | |
| if [ -d "$item" ] || [ -f "$item" ]; then | |
| docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \ | |
| "rm -rf /workspace/$item" 2>/dev/null || \ | |
| sudo rm -rf "$item" 2>/dev/null || true | |
| fi | |
| done | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| clean: true | |
| - name: Set UID/GID | |
| run: | | |
| echo "USER_ID=$(id -u)" >> $GITHUB_ENV | |
| echo "GROUP_ID=$(id -g)" >> $GITHUB_ENV | |
| - name: Create output directory | |
| run: mkdir -p release-binaries | |
| - name: Build oasis-app and oasis-ffi binaries (release) via container | |
| run: | | |
| ARCH=$(uname -m) | |
| echo "Building oasis_os binaries for linux-${ARCH}..." | |
| docker compose --profile ci run --rm rust-ci bash -c " | |
| set -e | |
| cargo build --workspace --release | |
| for bin in oasis-app oasis-screenshot; do | |
| if [ -f \"target/release/\$bin\" ]; then | |
| cp \"target/release/\$bin\" \"/app/release-binaries/\${bin}-linux-${ARCH}\" | |
| echo \"Built \$bin for linux-${ARCH}\" | |
| else | |
| echo \"Warning: \$bin not found in release output\" | |
| fi | |
| done | |
| # FFI shared library | |
| for ext in so dylib dll; do | |
| if [ -f \"target/release/liboasis_ffi.\$ext\" ]; then | |
| cp \"target/release/liboasis_ffi.\$ext\" \"/app/release-binaries/liboasis_ffi-linux-${ARCH}.\$ext\" | |
| echo \"Built liboasis_ffi.\$ext for linux-${ARCH}\" | |
| fi | |
| done | |
| " | |
| - name: List built binaries | |
| run: | | |
| echo "Built binaries:" | |
| ls -la release-binaries/ | |
| file release-binaries/* || true | |
| - name: Upload binary artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: oasis-binaries | |
| path: release-binaries/* | |
| retention-days: 90 | |
| # -- Create GitHub Release ----------------------------------------------- | |
| create-release: | |
| name: Create GitHub Release | |
| needs: [build-release-binaries] | |
| if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.create_release == 'true' | |
| runs-on: self-hosted | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Pre-checkout cleanup | |
| run: | | |
| for item in outputs target release-binaries .git/index.lock; do | |
| if [ -d "$item" ] || [ -f "$item" ]; then | |
| docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \ | |
| "rm -rf /workspace/$item" 2>/dev/null || \ | |
| sudo rm -rf "$item" 2>/dev/null || true | |
| fi | |
| done | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download binary artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: oasis-binaries | |
| path: release-artifacts/binaries | |
| - name: Download PSP EBOOT artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: psp-eboot | |
| path: release-artifacts/psp | |
| - name: Determine version | |
| id: version | |
| run: | | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION="${GITHUB_REF#refs/tags/}" | |
| else | |
| VERSION="v$(date +%Y.%m.%d)-$(echo ${{ github.sha }} | cut -c1-7)" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Version: $VERSION" | |
| - name: Generate changelog from merged PRs | |
| id: changelog | |
| run: | | |
| CURRENT_TAG="${{ steps.version.outputs.version }}" | |
| # Find the previous release tag for comparison | |
| PREVIOUS_TAG=$(git tag --sort=-version:refname --list 'v[0-9]*' | grep -Fxv "$CURRENT_TAG" | head -n 1) | |
| CHANGELOG="" | |
| if [ -n "$PREVIOUS_TAG" ]; then | |
| echo "Generating changelog: ${PREVIOUS_TAG}...${CURRENT_TAG}" | |
| CHANGELOG=$(gh api repos/${{ github.repository }}/releases/generate-notes \ | |
| -f tag_name="$CURRENT_TAG" \ | |
| -f previous_tag_name="$PREVIOUS_TAG" \ | |
| --jq '.body' 2>/dev/null || echo "") | |
| fi | |
| if [ -n "$CHANGELOG" ]; then | |
| printf '%s\n' "$CHANGELOG" > changelog_snippet.md | |
| echo "has_changelog=true" >> $GITHUB_OUTPUT | |
| echo "Changelog generated successfully" | |
| else | |
| echo "No changelog generated" | |
| echo "has_changelog=false" >> $GITHUB_OUTPUT | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate release notes | |
| id: release_notes | |
| run: | | |
| cat << 'EOF' > release_notes.md | |
| ## Release ${{ steps.version.outputs.version }} | |
| ### OASIS_OS | |
| Pre-built binaries for the OASIS_OS framework: | |
| | Binary | Description | | |
| |--------|-------------| | |
| | `oasis-app` | Desktop entry point (SDL2 backend) | | |
| | `oasis-screenshot` | Screenshot capture utility | | |
| | `liboasis_ffi` | C-ABI shared library for UE5 and external integrations | | |
| | `EBOOT.PBP` | PSP homebrew binary (runs on real hardware and PPSSPP) | | |
| ### Installation | |
| ```bash | |
| # Make binaries executable | |
| chmod +x oasis-app-linux-* oasis-screenshot-linux-* | |
| # Run the desktop app | |
| ./oasis-app-linux-* | |
| ``` | |
| ### Requirements | |
| - SDL2 runtime libraries (`libsdl2`, `libsdl2-mixer`) | |
| ### Commit Information | |
| - **Commit**: ${{ github.sha }} | |
| - **Branch**: ${{ github.ref_name }} | |
| EOF | |
| # Append auto-generated changelog if available | |
| if [ "${{ steps.changelog.outputs.has_changelog }}" = "true" ] && [ -f changelog_snippet.md ]; then | |
| printf '\n---\n\n' >> release_notes.md | |
| cat changelog_snippet.md >> release_notes.md | |
| fi | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.version }} | |
| name: Release ${{ steps.version.outputs.version }} | |
| body_path: release_notes.md | |
| draft: false | |
| prerelease: ${{ contains(steps.version.outputs.version, '-') }} | |
| files: | | |
| release-artifacts/binaries/* | |
| release-artifacts/psp/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # -- Deploy GitHub Pages -------------------------------------------------- | |
| deploy-pages: | |
| name: Deploy GitHub Pages | |
| needs: ci | |
| if: github.ref == 'refs/heads/main' | |
| runs-on: self-hosted | |
| timeout-minutes: 15 | |
| concurrency: | |
| group: pages | |
| cancel-in-progress: false | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install wasm-pack | |
| run: | | |
| if ! command -v wasm-pack &>/dev/null; then | |
| curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh | |
| fi | |
| - name: Build WASM | |
| run: ./scripts/build-wasm.sh --release | |
| - name: Assemble site | |
| run: | | |
| rm -rf _site | |
| mkdir -p _site/demo | |
| rsync -a --exclude screenshots site/ _site/ | |
| cp -r screenshots/ _site/screenshots/ | |
| cp -r pkg/ _site/pkg/ | |
| cp www/index.js _site/demo/index.js | |
| - name: Upload Pages artifact | |
| uses: actions/upload-pages-artifact@v3 | |
| with: | |
| path: _site | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v4 |