name: Build on: push: branches: - master # For testing. - actions tags: - "v*.*" paths-ignore: - '*.{txt,md}' - 'Tools/**' - '.{editorconfig,gitattributes,gitignore}' - 'appveyor.yml' pull_request: branches: - master paths-ignore: - '*.{txt,md}' - 'Tools/**' - '.{editorconfig,gitattributes,gitignore}' - 'appveyor.yml' env: BUILD_CONFIGURATION: Release jobs: build-windows: strategy: fail-fast: false matrix: platform: [x64, ARM64] runs-on: windows-latest steps: - uses: actions/checkout@v7 # Only the FFmpeg libraries this build uses. See the script. - name: Check out submodules shell: bash run: .github/checkout-submodules.sh ${{ matrix.platform == 'x64' && '/Windows/x86_64/' || '/Windows/aarch64/' }} /WindowsInclude/ - name: Add MSBuild to PATH uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 - name: Check Valid Version Tags id: valid-tags shell: bash run: | # This is required for git describe --always to work for git-version.cpp. git fetch --deepen=15000 --no-recurse-submodules --tags || exit 0 echo "count=$(git tag -l 'v[0-9]*' | wc -l | tr -d ' ')" >> $GITHUB_OUTPUT # $env:GITHUB_OUTPUT on pwsh - name: Fetch upstream tags # Fallback to fetching tags from upstream if: steps.valid-tags.outputs.count == '0' shell: bash run: | # TODO: should try to fetch tags from whereever this repo was forked from before fetching from official repo git remote add upstream https://github.com/hrydgard/ppsspp.git git fetch --deepen=15000 --no-recurse-submodules --tags --force upstream || exit 0 - name: Create git-version.cpp for Windows # Not sure why the one at git-version-gen.cmd couldn't get the version properly. #if: github.ref_type == 'tag' run: | $GIT_VERSION=git describe --always echo "Num of Valid Tags = ${{ steps.valid-tags.outputs.count }}" echo "Test GitVer = ${{ github.ref_name }} / $GIT_VERSION / ${GITHUB_REF_NAME}" echo "const char *PPSSPP_GIT_VERSION = `"$GIT_VERSION`";" > git-version.cpp echo "#define PPSSPP_GIT_VERSION_NO_UPDATE 1" >> git-version.cpp # Generate Windows/win-version.h too. if ($GIT_VERSION.StartsWith("v")) { $WIN_VERSION_COMMA=$GIT_VERSION -replace '^v', '' -replace '-g[0-9a-f]+$', '' -replace '[-\.]', ',' echo "Test WinVer = $WIN_VERSION_COMMA" echo "#define PPSSPP_WIN_VERSION_STRING `"$GIT_VERSION`"" > Windows/win-version.h echo "#define PPSSPP_WIN_VERSION_COMMA $WIN_VERSION_COMMA" >> Windows/win-version.h echo "#define PPSSPP_WIN_VERSION_NO_UPDATE 1" >> Windows/win-version.h } - name: Build Windows working-directory: ${{ env.GITHUB_WORKSPACE }} run: msbuild /m /p:TrackFileAccess=false /p:Configuration=${{ env.BUILD_CONFIGURATION }} /p:Platform=${{ matrix.platform }} Windows/PPSSPP.sln - name: Package build working-directory: ${{ env.GITHUB_WORKSPACE }} run: | mkdir ppsspp cp PPSSPP*.exe ppsspp/ cp *.pdb ppsspp/ cp Windows/*.bat ppsspp/ cp -r assets ppsspp/assets - name: Package headless (non-Win32) if: matrix.platform != 'Win32' run: cp Windows/${{ matrix.platform }}/Release/*.exe ppsspp/ - name: Package headless (Win32) if: matrix.platform == 'Win32' run: cp Windows/Release/*.exe ppsspp/ - name: Upload build uses: actions/upload-artifact@v7 with: name: Windows ${{ matrix.platform }} build path: ppsspp/ - name: Create release if: github.ref_type == 'tag' working-directory: ${{ env.GITHUB_WORKSPACE }} run: | rm ppsspp/PPSSPPHeadless* rm ppsspp/UnitTest* rm ppsspp/*.pdb mkdir releases Compress-Archive -Path "ppsspp/*" -Update -DestinationPath "releases/PPSSPP-${{ github.ref_name }}-Windows-${{ matrix.platform }}.zip" - name: Upload release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 if: github.ref_type == 'tag' with: files: releases/*.zip build-uwp: runs-on: windows-latest steps: - uses: actions/checkout@v7 # Only the FFmpeg libraries this build uses. See the script. - name: Check out submodules shell: bash run: .github/checkout-submodules.sh /Windows10/x64/ /WindowsInclude/ - name: Add MSBuild to PATH uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 - name: Restore NuGet packages working-directory: ${{ github.workspace }}/UWP run: nuget restore PPSSPP_UWP.sln - name: Build UWP working-directory: ${{ env.GITHUB_WORKSPACE }} run: msbuild /m /p:TrackFileAccess=false /p:Configuration=${{ env.BUILD_CONFIGURATION }} /p:Platform=x64 /p:AppxPackageSigningEnabled=false UWP/PPSSPP_UWP.sln # Mirrors the two Windows jobs in libretro's own GitLab CI (see .gitlab-ci.yml). The libretro core # is built by GNU make driving cl.exe/link.exe from an MSYS2 shell, not by the VS solution, so it # catches MSVC-specific errors that neither build-windows nor the Linux/Android libretro builds do # - duplicate symbols in particular, since MSVC emits inline functions that gcc/clang inline away. build-libretro-windows: name: build-libretro-windows (${{ matrix.arch }}) strategy: fail-fast: false matrix: include: - arch: x64 platform: windows_msvc2019_desktop_x64 msystem: MINGW64 - arch: x86 platform: windows_msvc2019_desktop_x86 msystem: MINGW32 runs-on: windows-latest timeout-minutes: 90 steps: - uses: actions/checkout@v7 # Only the FFmpeg libraries this build uses. See the script. - name: Check out submodules shell: bash run: .github/checkout-submodules.sh /Windows/${{ matrix.arch == 'x64' && 'x86_64' || 'x86' }}/ # Needs a real MSYS2 (make, bash, cygpath) - the one preinstalled on the runner image isn't on # PATH and has no make. path-type: inherit keeps the Windows PATH, which the Makefile needs for # reg.exe (Windows SDK lookup) and git. - name: Setup MSYS2 uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2.33.0 with: msystem: ${{ matrix.msystem }} path-type: inherit install: make # Deprecated in favour of UCRT64/CLANG64, but only make and bash come from MSYS2 here, # the compiler is MSVC. suppress-deprecation-warnings: msystem-mingw64 msystem-mingw32 # The Makefile can locate Visual Studio by itself, but only through "cmd //c bash VSWhere.sh", # which depends on COMSPEC and ProgramFiles(x86) surviving into the MSYS2 shell. Resolving it # here and passing VsInstallRoot= on the command line (a command-line variable overrides the # Makefile's own := assignment) fails loudly rather than silently misconfiguring the build. - name: Build libretro core shell: msys2 {0} run: | VS_PATH=$(sh libretro/VSWhere.sh -latest -property installationPath) test -n "$VS_PATH" || { echo "vswhere found no Visual Studio installation"; exit 1; } echo "Visual Studio: $VS_PATH" make -C libretro -f Makefile -j4 platform=${{ matrix.platform }} VsInstallRoot="$(cygpath -u "$VS_PATH")" test-windows: runs-on: windows-latest needs: build-windows timeout-minutes: 30 steps: - uses: actions/checkout@v7 with: submodules: false - name: Fetch tests working-directory: ${{ env.GITHUB_WORKSPACE }} # Doing this to avoid ffmpeg and other large submodules. run: git submodule update --init pspautotests assets/lang - name: Download build uses: actions/download-artifact@v8 with: name: Windows x64 build path: ppsspp/ - name: Execute unit tests working-directory: ${{ env.GITHUB_WORKSPACE }} run: ppsspp\\UnitTest.exe ALL - name: Execute headless tests working-directory: ${{ env.GITHUB_WORKSPACE }} run: python test.py -g --graphics=software build: name: build (${{ matrix.id }}) strategy: fail-fast: false matrix: include: - os: ubuntu-26.04 extra: test cc: clang cxx: clang++ args: ./b.sh --headless --unittest id: clang-normal ffmpeg: /linux/x86_64/ - os: ubuntu-26.04 cc: gcc cxx: g++ args: ./b.sh --headless --unittest id: gcc-normal ffmpeg: /linux/x86_64/ # Gives the headless tests an arm64 host to run the CPU backends on - the arm64 JIT is a # different backend from the x86-64 one, and nothing else in the matrix tests it on Linux. - os: ubuntu-26.04-arm extra: test cc: clang cxx: clang++ args: ./b.sh --headless --unittest id: clang-arm64 ffmpeg: /linux/aarch64/ - os: ubuntu-26.04 extra: android cc: clang cxx: clang++ args: cd android && ./ab.sh APP_ABI=arm64-v8a UNITTEST=1 HEADLESS=1 id: android-arm64 ffmpeg: /android/arm64/ - os: ubuntu-26.04 extra: android cc: clang cxx: clang++ args: cd android && ./ab.sh APP_ABI=armeabi-v7a UNITTEST=1 HEADLESS=1 id: android-arm32 ffmpeg: /android/armv7/ - os: ubuntu-26.04 extra: android cc: clang cxx: clang++ args: cd android && ./ab.sh APP_ABI=x86_64 UNITTEST=1 HEADLESS=1 id: android-x86_64 ffmpeg: /android/x86_64/ - os: ubuntu-26.04 extra: android cc: clang cxx: clang++ args: cd android && ./ab.sh APP_ABI=arm64-v8a OPENXR=1 id: android-vr ffmpeg: /android/arm64/ - os: ubuntu-26.04 extra: android cc: clang cxx: clang++ args: ./b.sh --libretro_android ppsspp_libretro id: android-libretro ffmpeg: /android/ - os: ubuntu-26.04 extra: libretro cc: gcc cxx: g++ args: make -C libretro -f Makefile -j2 id: gcc-libretro ffmpeg: /linux/x86_64/ - os: ubuntu-26.04 extra: libretro cc: clang cxx: clang++ args: make -C libretro -f Makefile -j2 id: clang-libretro ffmpeg: /linux/x86_64/ - os: ubuntu-26.04 extra: loongarch64 cc: gcc cxx: g++ args: ./b.sh --loongarch64 --unittest PPSSPPHeadless PPSSPPUnitTest id: loongarch64 ffmpeg: /linux/loongarch64/ - os: ubuntu-26.04 extra: riscv64 cc: gcc cxx: g++ args: ./b.sh --riscv64 --unittest PPSSPPHeadless PPSSPPUnitTest id: riscv64 ffmpeg: /linux/riscv64/ - os: macos-latest extra: test cc: clang cxx: clang++ args: ./b.sh --headless --unittest --no-png id: macos ffmpeg: /macosx/universal/ - os: macos-latest extra: libretro_mac cc: clang cxx: clang++ args: ./b.sh --libretro id: macos-libretro ffmpeg: /macosx/universal/ - os: macos-latest extra: ios cc: clang cxx: clang++ args: ./b-ios.sh id: ios ffmpeg: /ios/universal/ - os: macos-latest extra: tvos cc: clang cxx: clang++ args: ./b-tvos.sh id: tvos ffmpeg: /tvos/arm64/ # One job for both, since only five macOS jobs run at once and the rest wait in line. - os: macos-latest extra: libretro_ios_tvos cc: clang cxx: clang++ # libretro's buildbot uses Xcode 16.0, whose clang doesn't predefine TARGET_OS_*. Make ours # behave the same, so code that forgets to include TargetConditionals.h fails here too. # (CFLAGS from the environment doesn't work: the toolchain file overrides it.) args: export CMAKE_ARGS="-DCMAKE_C_FLAGS=-fno-define-target-os-macros -DCMAKE_CXX_FLAGS=-fno-define-target-os-macros" && ./b.sh --ios --libretro && ./b.sh --tvos --libretro id: ios-tvos-libretro ffmpeg: /ios/universal/ /tvos/arm64/ runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 # Only the FFmpeg libraries this build uses. See the script. - name: Check out submodules shell: bash run: .github/checkout-submodules.sh ${{ matrix.ffmpeg }} - name: Fetch tags for macOS releases if: runner.os == 'macOS' # This is required for git describe --always to work for git-version.cpp. run: | git fetch --deepen=15000 --no-recurse-submodules --tags || exit 0 - name: Setup Android NDK uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 if: matrix.extra == 'android' id: setup-ndk with: ndk-version: r29 - name: Install Linux dependencies if: runner.os == 'Linux' && matrix.extra != 'android' && matrix.extra != 'loongarch64' && matrix.extra != 'riscv64' run: | sudo apt-get update -y -qq sudo apt-get install libsdl3-dev libgl1-mesa-dev libglu1-mesa-dev libsdl3-ttf-dev libfontconfig1-dev libfribidi-dev libcurl4-openssl-dev - name: Install macOS SDL3 dependencies if: runner.os == 'macOS' && matrix.id == 'macos' run: brew install sdl3 sdl3_ttf - name: Install cross-compilation dependencies if: matrix.extra == 'loongarch64' || matrix.extra == 'riscv64' run: | sudo apt-get update -y -qq sudo apt-get install -y gcc-14-${{ matrix.extra }}-linux-gnu g++-14-${{ matrix.extra }}-linux-gnu binutils-${{ matrix.extra }}-linux-gnu libgl-dev libglu1-mesa-dev qemu-user sudo cmake/scripts/setup-cross.sh ${{ matrix.extra }} - name: Install iOS dependencies if: matrix.id == 'ios' run: | brew install ldid dpkg pillow # Check Xcode version xcodebuild -version - name: Create iOS Version.txt file if: matrix.id == 'ios' run: | # Create Version.txt file (should probably do this during building process) mkdir -p build-ios/PPSSPP.app GIT_VERSION=$(git describe --always --tags) echo ${GIT_VERSION#v} > build-ios/PPSSPP.app/Version.txt - name: Create macOS & iOS git-version.cpp for tagged release if: github.ref_type == 'tag' && (matrix.id == 'macos' || matrix.id == 'ios') run: | echo "const char *PPSSPP_GIT_VERSION = \"${GITHUB_REF_NAME}\";" > git-version.cpp echo "#define PPSSPP_GIT_VERSION_NO_UPDATE 1" >> git-version.cpp - name: Setup ccache uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d # v1.2.24 # Disable ccache on macos for now, it's become buggy for some reason. if: matrix.id != 'macos' with: key: ${{ matrix.id }} # Only master saves. The Actions cache holds 10 GB, and PR runs filled it with caches # only that PR could use, evicting master's, which every run restores from. save: ${{ github.ref == 'refs/heads/master' }} - name: Execute build env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} NDK: ${{ steps.setup-ndk.outputs.ndk-path }} NDK_CCACHE: ccache USE_CCACHE: 1 run: | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" export CCACHE_SLOPPINESS=clang_index_store,ivfsoverlay,include_file_ctime,include_file_mtime,modules,system_headers,time_macros if [ "${{ matrix.extra }}" == "android" ]; then export CCACHE_FILECLONE=true export CCACHE_DEPEND=true export CCACHE_COMPILERCHECK=content elif [ "$RUNNER_OS" == "macOS" ]; then export CCACHE_SLOPPINESS=pch_defines,$CCACHE_SLOPPINESS export CCACHE_FILECLONE=true export CCACHE_DEPEND=true export CCACHE_COMPILERCHECK=content fi ${{ matrix.args }} # These targets have no runner of their own, so the unit tests run under emulation. It's the # only coverage the loongarch64 and riscv64 JITs get - a compile is not much of a test for a # code generator. - name: Execute unit tests under qemu if: matrix.extra == 'loongarch64' || matrix.extra == 'riscv64' run: | qemu-${{ matrix.extra }} -L /usr/${{ matrix.extra }}-linux-gnu \ build-${{ matrix.extra }}/PPSSPPUnitTest all # Only the IR JIT - it's the sole native backend these two have, and the only thing here that # isn't shared code. The interpreters are portable C++ that the x86-64 and arm64 runners # already cover on all four backends, and every run costs emulated wall clock. - name: Execute headless tests under qemu if: matrix.extra == 'loongarch64' || matrix.extra == 'riscv64' run: | # test.py takes the newest build*/PPSSPPHeadless, so hand it one that runs the cross build # under emulation. The wall clock is raised because everything is slower under qemu. mkdir -p build-qemu printf '#!/bin/bash\nexec qemu-%s -L /usr/%s-linux-gnu "$(dirname "$0")/../build-%s/PPSSPPHeadless" "$@"\n' \ '${{ matrix.extra }}' '${{ matrix.extra }}' '${{ matrix.extra }}' > build-qemu/PPSSPPHeadless chmod +x build-qemu/PPSSPPHeadless python3 test.py -g --graphics=software --cpu=jit-ir --timeout=60 \ --known-failures=${{ matrix.extra }} - name: Package build if: matrix.extra == 'test' || matrix.id == 'ios' || matrix.id == 'tvos' run: | mkdir -p ppsspp if [ -e build*/$BUILD_CONFIGURATION/PPSSPPSDL ]; then cp build*/$BUILD_CONFIGURATION/PPSSPPSDL ppsspp/ elif [ -e build*/PPSSPPSDL.app ]; then cp -a build*/PPSSPPSDL.app ppsspp/ # GitHub Actions zipping kills symlinks and permissions. pushd ppsspp zip -qry PPSSPPSDL.zip PPSSPPSDL.app rm -rf PPSSPPSDL.app popd elif [ -e build-ios/PPSSPP.app ]; then # Get the final files generated by b-ios.h script find build -name 'PPSSPP*.ipa' -exec cp -a '{}' ppsspp/ \; find build -name 'PPSSPP*.xcarchive' -exec cp -a '{}' ppsspp/ \; find build -name 'PPSSPP*.deb' -exec cp -a '{}' ppsspp/ \; find build -name 'org.ppsspp*.deb' -exec cp -a '{}' ppsspp/ \; elif [ -e build-tvos-xcode/Release-appletvos/PPSSPP.app ]; then # Unsigned. Zipped, since artifact zipping loses symlinks and permissions. pushd build-tvos-xcode/Release-appletvos zip -qry ../../ppsspp/PPSSPP-tvOS.zip PPSSPP.app popd elif [ -e build*/PPSSPPSDL ]; then cp build*/PPSSPPSDL ppsspp/ cp -r assets ppsspp/assets fi if [ -e build*/$BUILD_CONFIGURATION/PPSSPPHeadless ]; then cp build*/$BUILD_CONFIGURATION/PPSSPPHeadless ppsspp/ elif [ -e build*/PPSSPPHeadless ]; then cp build*/PPSSPPHeadless ppsspp/ fi if [ -e build*/$BUILD_CONFIGURATION/PPSSPPUnitTest ]; then cp build*/$BUILD_CONFIGURATION/PPSSPPUnitTest ppsspp/ elif [ -e build*/PPSSPPUnitTest ]; then cp build*/PPSSPPUnitTest ppsspp/ fi - name: Upload build uses: actions/upload-artifact@v7 if: matrix.extra == 'test' with: name: ${{ matrix.os }} build path: ppsspp/ - name: Upload iOS build uses: actions/upload-artifact@v7 if: matrix.id == 'ios' || matrix.id == 'tvos' with: name: ${{ matrix.id }} build path: ppsspp/ - name: Create macOS release if: github.ref_type == 'tag' && matrix.id == 'macos' working-directory: ppsspp run: mv PPSSPPSDL.zip PPSSPPSDL-macOS-${GITHUB_REF_NAME}.zip - name: Upload macOS & iOS release uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 if: github.ref_type == 'tag' && (matrix.id == 'macos' || matrix.id == 'ios') with: files: | ppsspp/*.zip ppsspp/*.ipa ppsspp/*.deb body: > PPSSPP is a cross-platform PSP emulator. Visit PPSSPP [official website](https://ppsspp.org) for a [full changelog](https://ppsspp.org/index.html#news) as well as the [downloads section](https://ppsspp.org/downloads.html) for other platforms. # The test entries run the tests right here. A job of their own could only wait for the whole # build matrix, which held them back until the slowest unrelated build was done. These come # last, so the artifacts and releases above don't depend on them, as before. - name: Execute unit tests if: matrix.extra == 'test' timeout-minutes: 30 run: build/PPSSPPUnitTest ALL - name: Execute headless tests if: matrix.extra == 'test' && runner.os != 'Linux' timeout-minutes: 30 run: python3 test.py -g --graphics=software # The Linux runners carry the CPU backend coverage, on both x86-64 and arm64. Elsewhere the # default (JIT) is enough - the backends are shared code, and four runs everywhere adds up. # test.py picks a wall clock to match the backend, so the slower ones don't time out. - name: Execute headless tests (all CPU backends) if: matrix.extra == 'test' && runner.os == 'Linux' timeout-minutes: 30 run: | fail=0 for cpu in jit jit-ir ir interpreter; do echo "::group::CPU backend: $cpu" python3 test.py -g --graphics=software --cpu=$cpu || fail=1 echo "::endgroup::" done exit $fail # x86-64 only: the reference images don't match what the software renderer produces on arm64, # 23 of the 30 dumps differ there, one of them by an MSE of 100. That's a real difference and # worth chasing, but it predates the arm64 runner - it just never had a CI job to show up in. - name: Execute frametests if: matrix.id == 'clang-normal' timeout-minutes: 30 run: python3 frametests.py frametests/frametests.json --out-mode=failures - name: Upload frametest report uses: actions/upload-artifact@v7 if: matrix.id == 'clang-normal' && always() with: name: frametest-report path: frametests/out/ test-headless-alpine: runs-on: ubuntu-latest container: image: alpine:latest options: --shm-size=8g steps: - name: Install Linux dependencies (Alpine) # GNU tar: actions/cache runs tar --posix, which BusyBox tar rejects, so nothing was ever saved. run: | apk add build-base wget git bash cmake python3 glu-dev sdl3-dev sdl3_ttf-dev curl-dev tar zstd - uses: actions/checkout@v7 - name: Fix git detected dubious ownership in repository run: | chown -R $(id -u):$(id -g) $PWD # The FFmpeg sources, which this builds, without the prebuilt libraries. See the script. - name: Check out submodules run: | .github/checkout-submodules.sh '/*' '!/Windows/' '!/Windows10/' '!/WindowsInclude/' '!/android/' '!/blackberry/' '!/ios/' '!/linux/' '!/macosx/' '!/symbian/' '!/tvos/' '!/wiiu/' - name: Setup ccache uses: hendrikmuhs/ccache-action@f09c25b45002a07be2955cbe52e8cee55643f89d # v1.2.24 with: key: alpine # See the other ccache setup. save: ${{ github.ref == 'refs/heads/master' }} # The prebuilt Linux FFmpeg is linked against glibc, so this builds it for musl, into the same # folder. That takes a few minutes, so the result is cached until FFmpeg, its build script # or Alpine changes. - name: FFmpeg cache key id: ffmpeg-key run: | echo "key=alpine-ffmpeg-$(cat /etc/alpine-release)-$(git rev-parse HEAD:ffmpeg)-$(sha1sum ffmpeg/linux_x86-64.sh | cut -c1-12)" >> $GITHUB_OUTPUT - name: Restore FFmpeg id: ffmpeg-cache uses: actions/cache/restore@v6.1.0 with: path: ffmpeg/linux/x86_64 key: ${{ steps.ffmpeg-key.outputs.key }} - name: Compile ffmpeg if: steps.ffmpeg-cache.outputs.cache-hit != 'true' run: | cd ffmpeg && ./linux_x86-64.sh - name: Save FFmpeg if: steps.ffmpeg-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/master' uses: actions/cache/save@v6.1.0 with: path: ffmpeg/linux/x86_64 key: ${{ steps.ffmpeg-key.outputs.key }} - name: Build for testing run: | ./b.sh --headless --unittest - name: Execute unit tests run: | ./build/PPSSPPUnitTest ALL - name: Execute headless tests run: | python test.py -g --graphics=software