Fix: bundle Qt image format plugins in Linux AppImage #191
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: Build PollyMC-Continued | |
| on: | |
| push: | |
| branches: [main, develop] | |
| tags: ["v*"] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| jobs: | |
| skip-check: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| skip: ${{ steps.check.outputs.skip }} | |
| version: ${{ steps.check.outputs.version }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - id: check | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| COMMITS="" | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| COMMITS=$(git log --format="%s %b" "${{ github.event.pull_request.base.sha }}".."${{ github.event.pull_request.head.sha }}") | |
| elif [ "${{ github.event_name }}" = "push" ]; then | |
| COMMITS=$(git log --format="%s %b" "${{ github.event.before }}".."${{ github.event.after }}" 2>/dev/null || git log --format="%s %b" -1) | |
| else | |
| COMMITS=$(git log --format="%s %b" -1) | |
| fi | |
| echo "DEBUG: event_name=${{ github.event_name }}" | |
| echo "DEBUG: commits=${COMMITS:0:500}" | |
| if echo "$COMMITS" | grep -qF '[skip-all]'; then | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| echo "DEBUG: [skip-all] found, skipping entire workflow." | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| echo "DEBUG: No [skip-all] found." | |
| fi | |
| # --- Single source of truth for version number --- | |
| # Every downstream job (build-windows, build-linux, build-macos, release) | |
| # consumes this output instead of recomputing its own bump, so the | |
| # version baked into artifact filenames always matches the release tag. | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| VERSION="${{ github.ref_name }}" | |
| VERSION="${VERSION#v}" | |
| echo "DEBUG: Tag push detected, using tag version: ${VERSION}" | |
| else | |
| LATEST_TAG=$(git describe --tags --match "v*" --abbrev=0 2>/dev/null || echo "") | |
| if [ -z "$LATEST_TAG" ]; then | |
| # No tags exist yet in this repo - diff against full history instead | |
| # of a synthetic "v0.0.0" ref, which would make `git log` fail. | |
| BUMP_COMMITS=$(git log --format="%s %b" 2>/dev/null || echo "") | |
| LATEST_TAG="v0.0.0" | |
| else | |
| BUMP_COMMITS=$(git log "${LATEST_TAG}..HEAD" --format="%s %b" 2>/dev/null || echo "") | |
| fi | |
| VERSION=${LATEST_TAG#v} | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION" | |
| if echo "$BUMP_COMMITS" | grep -q "BREAKING CHANGE"; then | |
| MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 | |
| BUMP="major" | |
| elif echo "$BUMP_COMMITS" | grep -qE "^(feat|feature|Add|Build|Enhance)(\(|:|\ )"; then | |
| MINOR=$((MINOR + 1)); PATCH=0 | |
| BUMP="minor" | |
| else | |
| PATCH=$((PATCH + 1)) | |
| BUMP="patch" | |
| fi | |
| VERSION="${MAJOR}.${MINOR}.${PATCH}" | |
| echo "DEBUG: Latest tag: ${LATEST_TAG}" | |
| echo "DEBUG: Next version: ${VERSION} (${BUMP} bump)" | |
| fi | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| changelog-check: | |
| name: Verify changelog entry | |
| needs: [skip-check] | |
| if: needs.skip-check.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for matching CHANGELOG.md section | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| EXPECTED_TAG="${{ github.ref_name }}" | |
| else | |
| EXPECTED_TAG="v${{ needs.skip-check.outputs.version }}" | |
| fi | |
| HEAD_MSG=$(git log --format="%s %b" HEAD -1) | |
| if echo "$HEAD_MSG" | grep -qF '[skip changelog]'; then | |
| echo "Skipping changelog check: [skip changelog] found in the latest commit." | |
| exit 0 | |
| fi | |
| echo "Looking for '## ${EXPECTED_TAG}' in CHANGELOG.md ..." | |
| # Use a counted grep, not `grep -q`: -q exits on the first match and | |
| # closes the pipe, sed then dies with SIGPIPE and pipefail turns the | |
| # whole pipeline into a failure even though the section exists. | |
| if [ "$(sed 's/\r$//' CHANGELOG.md | grep -Fxc "## ${EXPECTED_TAG}" || true)" -eq 0 ]; then | |
| echo "::error::CHANGELOG.md has no '## ${EXPECTED_TAG}' section, but the computed release version is ${EXPECTED_TAG}." | |
| echo "::error::Add a '## ${EXPECTED_TAG}' section (Added/Changed/Fixed/Removed) to CHANGELOG.md, or add '[skip changelog]' to the commit message if this change doesn't need a release note." | |
| exit 1 | |
| fi | |
| echo "Found matching CHANGELOG.md section for ${EXPECTED_TAG}." | |
| build-windows: | |
| name: Windows (MinGW) | |
| needs: [skip-check] | |
| if: needs.skip-check.outputs.skip != 'true' | |
| runs-on: windows-latest | |
| permissions: | |
| contents: read | |
| env: | |
| # Set once at job level so it's visible in every step (including the | |
| # Build step, where ccache actually does its work) - a per-step | |
| # `export` doesn't survive to the next step in Actions. | |
| CCACHE_DIR: ${{ github.workspace }}/.ccache | |
| defaults: | |
| run: | |
| shell: msys2 {0} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Cache MSYS2 packages | |
| uses: actions/cache@v5 | |
| with: | |
| path: C:\msys64\var\cache\pacman\pkg | |
| # NOTE: hashFiles assumes this workflow file is saved as build.yml. | |
| # If it's saved under a different filename, hashFiles returns "" and | |
| # this key never changes when the workflow is edited. Verify the | |
| # actual path, or hash something more stable like CMakeLists.txt. | |
| key: msys2-v1 | |
| restore-keys: msys2- | |
| - name: Setup MSYS2 | |
| uses: msys2/setup-msys2@v2 | |
| with: | |
| msystem: MINGW64 | |
| update: true | |
| install: >- | |
| mingw-w64-x86_64-gcc | |
| mingw-w64-x86_64-cmake | |
| mingw-w64-x86_64-ninja | |
| mingw-w64-x86_64-qt6-base | |
| mingw-w64-x86_64-qt6-svg | |
| mingw-w64-x86_64-qt6-5compat | |
| mingw-w64-x86_64-qt6-networkauth | |
| mingw-w64-x86_64-qt6-websockets | |
| mingw-w64-x86_64-extra-cmake-modules | |
| mingw-w64-x86_64-tomlplusplus | |
| mingw-w64-x86_64-cmark | |
| mingw-w64-x86_64-qrencode | |
| mingw-w64-x86_64-libarchive | |
| mingw-w64-x86_64-zlib | |
| mingw-w64-x86_64-curl | |
| mingw-w64-x86_64-openssl | |
| mingw-w64-x86_64-libpng | |
| mingw-w64-x86_64-lz4 | |
| mingw-w64-x86_64-xz | |
| mingw-w64-x86_64-bzip2 | |
| mingw-w64-x86_64-zstd | |
| mingw-w64-x86_64-brotli | |
| mingw-w64-x86_64-nghttp2 | |
| mingw-w64-x86_64-libssh2 | |
| mingw-w64-x86_64-expat | |
| mingw-w64-x86_64-libiconv | |
| mingw-w64-x86_64-libb2 | |
| mingw-w64-x86_64-libidn2 | |
| mingw-w64-x86_64-sqlite3 | |
| mingw-w64-x86_64-nsis | |
| mingw-w64-x86_64-ccache | |
| - name: Cache ccache | |
| uses: actions/cache@v5 | |
| with: | |
| path: .ccache | |
| # Unique per run so a fresh save always succeeds (actions/cache | |
| # entries are immutable per key - reusing a static key means every | |
| # run after the first silently fails to save, so ccache never | |
| # learns about new commits and hit rate degrades over time). | |
| # restore-keys still picks up the most recent prior cache to warm | |
| # from. | |
| key: ccache-windows-${{ github.run_id }} | |
| restore-keys: | | |
| ccache-windows- | |
| - name: Setup Node.js | |
| shell: bash | |
| run: | | |
| # Node.js is available via MSYS2; just install deps | |
| cd bot-server | |
| npm install --omit=dev | |
| - name: Configure | |
| run: | | |
| set -euo pipefail | |
| cmake -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_INSTALL_PREFIX=install \ | |
| -DCMAKE_SYSTEM_LIBRARY_PATH=/mingw64/bin \ | |
| -DCMAKE_CXX_FLAGS="-Wno-error" \ | |
| -DCMAKE_C_FLAGS="-Wno-error" \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=ccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ | |
| -DLauncher_VERSION="${{ needs.skip-check.outputs.version }}" \ | |
| -DLauncher_BUILD_NUMBER="${{ github.run_number }}" \ | |
| -S . -B build | |
| - name: Build | |
| run: | | |
| set -euo pipefail | |
| ccache -z | |
| cmake --build build -j$(nproc) | |
| cmake --install build | |
| echo "=== ccache stats ===" | |
| ccache -s | |
| ccache --print-stats | |
| - name: Bundle bot-server | |
| run: | | |
| set -euo pipefail | |
| rm -rf install/bot-server | |
| cp -r bot-server install/bot-server | |
| rm -f install/bot-server/.gitignore | |
| # Remove any dev-only files to keep the bundle lean | |
| rm -rf install/bot-server/node_modules/.cache 2>/dev/null || true | |
| - name: Deploy Qt | |
| run: | | |
| set -euo pipefail | |
| cd install | |
| /mingw64/bin/windeployqt6.exe --release pollymc.exe | |
| SYSTEM_DLLS="^(KERNEL32|KERNELBASE|USER32|GDI32|gdi32full|ADVAPI32|SHELL32|ole32|OLEAUT32|msvcrt|msvcp_win|ntdll|WS2_32|SHLWAPI|WINMM|VERSION|d3d11|d3d12|dxgi|dwmapi|DWrite|UxTheme|USP10|COMCTL32|COMDLG32|IMAPI2|MAPI32|netapi32|PSAPI|Secur32|WINTRUST|CRYPT32|IMAGEHLP|DBGHELP|PDHINST|WTSAPI32|WININET|MSWSOCK|IPHLPAPI|WS2HELP|bcrypt|RPCRT4|AUTHZ|MPR|USERENV|DNSAPI|WINHTTP)\.dll$" | |
| NEED_FIX=0 | |
| for pass in $(seq 1 15); do | |
| ADDED=0 | |
| for f in *.exe *.dll; do | |
| [ -f "$f" ] || continue | |
| while IFS= read -r dll; do | |
| [ -z "$dll" ] && continue | |
| if echo "$dll" | grep -qiE "^(api-ms-win-|ext-ms-win-)"; then continue; fi | |
| if [ -f "$dll" ]; then continue; fi | |
| if echo "$dll" | grep -iqE "$SYSTEM_DLLS"; then continue; fi | |
| if [ -f "/c/Windows/System32/$dll" ]; then continue; fi | |
| if [ -f "/mingw64/bin/$dll" ]; then | |
| cp -f "/mingw64/bin/$dll" . | |
| echo "Pass $pass: deployed $dll (needed by $f)" | |
| ADDED=1 | |
| else | |
| echo "::error file=$f::$f is missing dependency: $dll" | |
| NEED_FIX=1 | |
| fi | |
| done < <(objdump -p "$f" 2>/dev/null | awk -F': ' '/^\s*DLL Name:/ {print $2}' | tr -d '\r') | |
| done | |
| [ "$ADDED" -eq 0 ] && break | |
| done | |
| echo "=== DLLs in install/ ===" | |
| ls -la *.dll | |
| [ "$NEED_FIX" -eq 0 ] || exit 1 | |
| - name: Build NSIS installer | |
| run: | | |
| set -euo pipefail | |
| BUILD_DIR_WIN="$(cygpath -w "$(pwd)/install")" | |
| VERSION="${{ needs.skip-check.outputs.version }}" | |
| # Fail loudly instead of silently shipping an installer with the | |
| # wrong name if the placeholder pattern doesn't match the .nsi file. | |
| if ! grep -q 'PollyMC-Continued-[^ ]*-Setup' pollymc_installer.nsi; then | |
| echo "::error::Expected placeholder 'PollyMC-Continued-*-Setup' not found in pollymc_installer.nsi" | |
| exit 1 | |
| fi | |
| sed "s/PollyMC-Continued-[^ ]*-Setup/PollyMC-Continued-${VERSION}-Windows-Setup/" \ | |
| pollymc_installer.nsi > pollymc_installer_versioned.nsi | |
| makensis \ | |
| "-DBUILD_DIR=${BUILD_DIR_WIN}" \ | |
| "-DVERSION=${VERSION}" \ | |
| "pollymc_installer_versioned.nsi" | |
| ls -la PollyMC-Continued-*-Setup.exe || (echo "ERROR: Installer not found!" && exit 1) | |
| - name: Test installed binary for missing DLLs | |
| shell: powershell | |
| run: | | |
| $installer = Get-ChildItem -Path "$env:GITHUB_WORKSPACE" -Filter "PollyMC-Continued-*-Setup.exe" | Select-Object -First 1 | |
| if (-not $installer) { throw "No installer found at $env:GITHUB_WORKSPACE" } | |
| $installDir = Join-Path $env:RUNNER_TEMP "PollyMC-Test" | |
| Write-Host "Silently installing $($installer.Name) to $installDir ..." | |
| $proc = Start-Process -FilePath $installer.FullName -ArgumentList "/S", "/D=$installDir" -Wait -PassThru -NoNewWindow | |
| Write-Host "Installer exit code: $($proc.ExitCode)" | |
| $exePath = Join-Path $installDir "pollymc.exe" | |
| if (-not (Test-Path $exePath)) { | |
| $exePath = Join-Path $installDir "PollyMC-Continued" "pollymc.exe" | |
| } | |
| if (-not (Test-Path $exePath)) { throw "pollymc.exe not found after installation" } | |
| $stderrFile = Join-Path $env:RUNNER_TEMP "pollymc_stderr.txt" | |
| if (Test-Path $stderrFile) { Remove-Item $stderrFile -Force } | |
| $startTime = Get-Date | |
| $process = Start-Process -FilePath $exePath -PassThru -RedirectStandardError $stderrFile | |
| $exited = $process.WaitForExit(10000) | |
| if (-not $exited) { | |
| $process.Kill() | |
| $process.WaitForExit() | |
| $events = Get-WinEvent -FilterHashtable @{LogName='Application'; StartTime=$startTime} -MaxEvents 10 -ErrorAction SilentlyContinue | |
| $hasError = $false | |
| foreach ($event in $events) { | |
| if ($event.Message -match "pollymc") { | |
| Write-Host "::error::Application Error detected" | |
| Write-Host "Event: $($event.TimeCreated) -- $($event.Message)" | |
| if ($event.Message -match "([a-zA-Z0-9_-]+\.dll)") { | |
| Write-Host "::error::Missing or faulty module: $($matches[1])" | |
| } | |
| $hasError = $true | |
| } | |
| } | |
| if (-not $hasError) { | |
| Write-Host "pollymc.exe ran for 10s with no DLL errors" | |
| } else { | |
| throw "Missing DLLs detected in pollymc.exe" | |
| } | |
| } else { | |
| $exitCode = $process.ExitCode | |
| if ($exitCode -eq -1073741515) { | |
| Write-Host "::error::STATUS_DLL_NOT_FOUND (0xC0000135) -- a required DLL is missing" | |
| $events = Get-WinEvent -FilterHashtable @{LogName='Application'; StartTime=$startTime} -MaxEvents 10 -ErrorAction SilentlyContinue | |
| foreach ($event in $events) { | |
| if ($event.Message -match "pollymc|$([regex]::Escape($exePath))") { | |
| Write-Host $event.Message | |
| if ($event.Message -match "(?i)([a-zA-Z0-9_-]+\.dll)") { | |
| Write-Host "::error::Missing DLL: $($matches[1])" | |
| } | |
| } | |
| } | |
| throw "pollymc.exe failed -- missing DLL (STATUS_DLL_NOT_FOUND)" | |
| } else { | |
| Write-Host "pollymc.exe exited with code $exitCode (DLLs loaded OK)" | |
| } | |
| } | |
| if (Test-Path $stderrFile) { | |
| $stderr = Get-Content $stderrFile | |
| if ($stderr) { Write-Host "=== stderr ===`n$stderr" } | |
| } | |
| - name: Zip portable build | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ needs.skip-check.outputs.version }}" | |
| cd install | |
| ls -la pollymc.exe || (echo "ERROR: pollymc.exe not found in install!" && exit 1) | |
| # portable.txt marks a portable install (data dir + updater asset matching depend on it), | |
| # so the portable zip must contain it next to pollymc.exe | |
| cp "$GITHUB_WORKSPACE/program_info/portable.txt" portable.txt | |
| # manifest.txt lists every top-level entry of the install; the updater installs exactly | |
| # this list, so files that are new to a release (like portable.txt) actually get deployed | |
| find . -mindepth 1 -maxdepth 1 -printf '%P\n' | sort > manifest.txt | |
| "/c/Program Files/7-Zip/7z.exe" a \ | |
| "../PollyMC-Continued-${VERSION}-Windows-portable.zip" . | |
| cd .. | |
| ls -la PollyMC-Continued-*-Windows-portable.zip || (echo "ERROR: Zip not found!" && exit 1) | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: PollyMC-Continued-Windows | |
| path: | | |
| PollyMC-Continued-*-Setup.exe | |
| PollyMC-Continued-*-Windows-portable.zip | |
| build-linux: | |
| name: Linux | |
| needs: [skip-check] | |
| if: needs.skip-check.outputs.skip != 'true' | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| env: | |
| APPIMAGE_EXTRACT_AND_RUN: "1" | |
| # Set once at job level so it's visible in every step (including the | |
| # Build step, where ccache actually does its work) - a per-step | |
| # `export` doesn't survive to the next step in Actions. Explicit path | |
| # also avoids relying on ccache's default cache dir, which changed | |
| # location as of ccache 4.0 (now ~/.cache/ccache on Linux). | |
| CCACHE_DIR: ${{ github.workspace }}/.ccache | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install Dependencies | |
| run: | | |
| set -euo pipefail | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| build-essential cmake ninja-build pkg-config \ | |
| qt6-base-dev qt6-svg-dev qt6-5compat-dev qt6-networkauth-dev \ | |
| qt6-websockets-dev qt6-base-dev-tools qt6-tools-dev libgl-dev \ | |
| qt6-image-formats-plugins \ | |
| extra-cmake-modules \ | |
| libtomlplusplus-dev libcmark-dev libqrencode-dev cmark \ | |
| libquazip1-qt6-dev libarchive-dev zlib1g-dev \ | |
| libcurl4-openssl-dev libssl-dev libpng-dev \ | |
| liblz4-dev liblzma-dev libbz2-dev libzstd-dev \ | |
| gamemode-dev \ | |
| patchelf \ | |
| ccache \ | |
| breeze | |
| - name: Cache ccache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ${{ env.CCACHE_DIR }} | |
| # Unique per run so a fresh save always succeeds (actions/cache | |
| # entries are immutable per key - reusing a static key means every | |
| # run after the first silently fails to save, so ccache never | |
| # learns about new commits and hit rate degrades over time). | |
| # restore-keys still picks up the most recent prior cache to warm | |
| # from. | |
| key: ccache-linux-${{ github.run_id }} | |
| restore-keys: | | |
| ccache-linux- | |
| - name: Setup Node.js | |
| run: | | |
| # Node.js ships preinstalled on GitHub-hosted runners; just install deps | |
| cd bot-server | |
| npm install --omit=dev | |
| - name: Configure | |
| run: | | |
| set -euo pipefail | |
| cmake -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_INSTALL_PREFIX=/usr \ | |
| -DCMAKE_CXX_FLAGS="-Wno-error" \ | |
| -DCMAKE_C_FLAGS="-Wno-error" \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=ccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ | |
| -DLauncher_VERSION="${{ needs.skip-check.outputs.version }}" \ | |
| -DLauncher_BUILD_NUMBER="${{ github.run_number }}" \ | |
| -S . -B build | |
| - name: Build | |
| run: | | |
| set -euo pipefail | |
| ccache -z | |
| cmake --build build -j$(nproc) | |
| echo "=== ccache stats ===" | |
| ccache -s | |
| ccache --print-stats | |
| - name: Package | |
| run: | | |
| set -euo pipefail | |
| cmake --install build --prefix /tmp/pollymc | |
| mkdir -p appdir/usr/bin appdir/usr/lib appdir/usr/share/applications appdir/usr/share/icons/hicolor/256x256/apps | |
| cp /tmp/pollymc/bin/pollymc appdir/usr/bin/ | |
| cp /tmp/pollymc/bin/pollymc_filelink appdir/usr/bin/ 2>/dev/null || true | |
| cp -r /tmp/pollymc/share/* appdir/usr/share/ 2>/dev/null || true | |
| cp build/program_info/org.pollymc.PollyMC.desktop appdir/usr/share/applications/org.pollymc.PollyMC.desktop | |
| cp program_info/org.pollymc.PollyMC_256.png appdir/usr/share/icons/hicolor/256x256/apps/org.pollymc.PollyMC.png | |
| # Bundle critical libraries explicitly | |
| cp -L /usr/lib/x86_64-linux-gnu/libssl.so.* appdir/usr/lib/ 2>/dev/null || true | |
| cp -L /usr/lib/x86_64-linux-gnu/libcrypto.so.* appdir/usr/lib/ 2>/dev/null || true | |
| cp -L /usr/lib/x86_64-linux-gnu/libgamemode.so.* appdir/usr/lib/ 2>/dev/null || true | |
| cp -L /usr/lib/x86_64-linux-gnu/libgamemodeauto.so.* appdir/usr/lib/ 2>/dev/null || true | |
| # Bundle all Qt image format plugins | |
| mkdir -p appdir/usr/plugins/imageformats | |
| cp -L /usr/lib/x86_64-linux-gnu/qt6/plugins/imageformats/*.so appdir/usr/plugins/imageformats/ 2>/dev/null || true | |
| # Bundle the Qt icon engine plugin (libqsvgicon.so) so SVG icons | |
| # render in menus/toolbars. Without it, QIcon::fromTheme() SVG | |
| # icons silently fail to load. imageformats/libqsvg.so alone is not | |
| # enough - it only handles SVG as a raster image. | |
| mkdir -p appdir/usr/plugins/iconengines | |
| for p in /usr/lib/x86_64-linux-gnu/qt6/plugins/iconengines/*.so /usr/lib/qt6/plugins/iconengines/*.so; do | |
| if [ -f "$p" ]; then | |
| cp -L "$p" appdir/usr/plugins/iconengines/ | |
| echo "DEBUG: bundled icon engine plugin: $p" | |
| fi | |
| done | |
| - name: Bundle bot-server | |
| run: | | |
| set -euo pipefail | |
| # applicationDirPath() = appdir/usr/bin at runtime inside the AppImage | |
| cp -r bot-server appdir/usr/bin/bot-server | |
| rm -f appdir/usr/bin/bot-server/.gitignore | |
| # Remove any dev-only files to keep the bundle lean | |
| rm -rf appdir/usr/bin/bot-server/node_modules/.cache 2>/dev/null || true | |
| - name: Cache linuxdeploy | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| linuxdeploy-x86_64.AppImage | |
| linuxdeploy-plugin-qt-x86_64.AppImage | |
| key: linuxdeploy-continuous-${{ runner.os }} | |
| restore-keys: | | |
| linuxdeploy-continuous- | |
| - name: Create AppImage | |
| run: | | |
| set -ex | |
| if [ ! -f linuxdeploy-x86_64.AppImage ]; then | |
| wget -v "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" -O linuxdeploy-x86_64.AppImage | |
| fi | |
| if [ ! -f linuxdeploy-plugin-qt-x86_64.AppImage ]; then | |
| wget -v "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" -O linuxdeploy-plugin-qt-x86_64.AppImage | |
| fi | |
| chmod +x linuxdeploy-x86_64.AppImage linuxdeploy-plugin-qt-x86_64.AppImage | |
| cp program_info/org.pollymc.PollyMC_256.png appdir/org.pollymc.PollyMC.png | |
| cp build/program_info/org.pollymc.PollyMC.desktop appdir/org.pollymc.PollyMC.desktop | |
| # Bundle the KDE Breeze widget style (a kstyle plugin, not shipped by | |
| # Qt itself). Placing it in appdir/usr/plugins/qt/styles BEFORE | |
| # linuxdeploy runs lets linuxdeploy-plugin-qt pick it up and bundle | |
| # its dependencies. Ubuntu 24.04 package: "breeze". The Qt6 plugin is | |
| # usually named breeze6.so (or breeze.so on older releases). | |
| BREEZE_STYLE="" | |
| for p in /usr/lib/x86_64-linux-gnu/qt6/plugins/styles/breeze*.so /usr/lib/qt6/plugins/styles/breeze*.so; do | |
| if [ -f "$p" ]; then BREEZE_STYLE="$p"; break; fi | |
| done | |
| if [ -n "$BREEZE_STYLE" ]; then | |
| mkdir -p appdir/usr/plugins/qt/styles | |
| cp -L "$BREEZE_STYLE" appdir/usr/plugins/qt/styles/ | |
| echo "DEBUG: bundled Breeze style plugin: $BREEZE_STYLE" | |
| else | |
| echo "::warning::Breeze Qt6 style plugin not found on runner; only stock Qt styles will be available" | |
| fi | |
| QMAKE=$(command -v qmake6 || command -v qmake) | |
| if [ -z "$QMAKE" ]; then | |
| echo "::error::qmake not found in PATH" | |
| exit 1 | |
| fi | |
| echo "DEBUG: QMAKE=$QMAKE" | |
| QMAKE="$QMAKE" ./linuxdeploy-x86_64.AppImage \ | |
| --appdir appdir \ | |
| --plugin qt \ | |
| --output appimage | |
| - name: Rename AppImage | |
| run: | | |
| set -euo pipefail | |
| FILE=$(ls PollyMC*.AppImage 2>/dev/null | head -n1) | |
| if [ -z "$FILE" ]; then | |
| echo "ERROR: No AppImage found. Listing directory:"; ls -la; exit 1 | |
| fi | |
| mv "$FILE" "PollyMC-Continued-${{ needs.skip-check.outputs.version }}-Linux-x86_64.AppImage" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: PollyMC-Continued-Linux-AppImage | |
| path: PollyMC-Continued-${{ needs.skip-check.outputs.version }}-Linux-x86_64.AppImage | |
| build-macos: | |
| name: macOS (Universal) | |
| needs: [skip-check] | |
| if: needs.skip-check.outputs.skip != 'true' | |
| runs-on: macos-14 | |
| permissions: | |
| contents: read | |
| env: | |
| BUILD_TYPE: Release | |
| MACOSX_DEPLOYMENT_TARGET: "13.0" | |
| # Set once at job level so it's visible in every step (including the | |
| # Build step, where ccache actually does its work) - a per-step | |
| # `export` doesn't survive to the next step in Actions. Explicit path | |
| # also avoids relying on ccache's default cache dir, which changed | |
| # location as of ccache 4.0 (now ~/Library/Caches/ccache on macOS). | |
| CCACHE_DIR: ${{ github.workspace }}/.ccache | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install build tools | |
| run: | | |
| set -euo pipefail | |
| brew update | |
| brew install ninja extra-cmake-modules cmark qrencode libarchive tomlplusplus lz4 xz zstd libpng ccache | |
| - name: Install Qt | |
| uses: jurplel/install-qt-action@v4 | |
| with: | |
| aqtsource: git+https://github.com/miurahr/aqtinstall.git | |
| version: 6.11.* | |
| modules: qtimageformats qtnetworkauth qtwebsockets | |
| cache: true | |
| - name: Cache ccache | |
| uses: actions/cache@v5 | |
| with: | |
| path: ${{ env.CCACHE_DIR }} | |
| # Unique per run so a fresh save always succeeds (actions/cache | |
| # entries are immutable per key - reusing a static key means every | |
| # run after the first silently fails to save, so ccache never | |
| # learns about new commits and hit rate degrades over time). | |
| # restore-keys still picks up the most recent prior cache to warm | |
| # from. | |
| key: ccache-macos-${{ github.run_id }} | |
| restore-keys: | | |
| ccache-macos- | |
| - name: Setup Node.js | |
| run: | | |
| # Node.js ships preinstalled on GitHub-hosted runners; just install deps | |
| cd bot-server | |
| npm install --omit=dev | |
| - name: Configure | |
| run: | | |
| set -euo pipefail | |
| export PKG_CONFIG_PATH="/opt/homebrew/opt/libarchive/lib/pkgconfig:$PKG_CONFIG_PATH" | |
| cmake -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_INSTALL_PREFIX=install \ | |
| -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" \ | |
| -DCMAKE_OSX_ARCHITECTURES="arm64" \ | |
| -DLauncher_BUILD_RELEASE=ON \ | |
| -DLauncher_BUILD_PLATFORM="${{ runner.os }}" \ | |
| -DLauncher_ENABLE_JAVA_DOWNLOADER=ON \ | |
| -DENABLE_LTO=ON \ | |
| -DCMAKE_C_COMPILER_LAUNCHER=ccache \ | |
| -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ | |
| -DLauncher_VERSION="${{ needs.skip-check.outputs.version }}" \ | |
| -DLauncher_BUILD_NUMBER="${{ github.run_number }}" \ | |
| -S . -B build | |
| - name: Read and validate version | |
| id: version | |
| run: | | |
| set -euo pipefail | |
| VERSION=$(cmake -N -LA build | sed -n 's/^Launcher_VERSION:STRING=//p') | |
| if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| echo "Invalid Launcher_VERSION: $VERSION" >&2 | |
| exit 1 | |
| fi | |
| if [[ "$GITHUB_REF" == refs/tags/v* && "v$VERSION" != "$GITHUB_REF_NAME" ]]; then | |
| echo "Tag $GITHUB_REF_NAME does not match launcher version $VERSION" >&2 | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Build | |
| run: | | |
| ccache -z | |
| cmake --build --preset macos_universal --config "$BUILD_TYPE" | |
| echo "=== ccache stats ===" | |
| ccache -s | |
| ccache --print-stats | |
| - name: Import signing certificate | |
| env: | |
| APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }} | |
| APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} | |
| APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -z "$APPLE_CODESIGN_CERT" && -z "$APPLE_CODESIGN_ID" ]]; then | |
| echo "No Apple signing certificate configured; the artifact will be ad-hoc signed." | |
| exit 0 | |
| fi | |
| if [[ -z "$APPLE_CODESIGN_CERT" || -z "$APPLE_CODESIGN_ID" || -z "$APPLE_CODESIGN_PASSWORD" ]]; then | |
| echo "Apple signing secrets are only partially configured." >&2 | |
| exit 1 | |
| fi | |
| KEYCHAIN_PATH="$RUNNER_TEMP/pollymc-build.keychain-db" | |
| CERTIFICATE_PATH="$RUNNER_TEMP/pollymc-codesign.p12" | |
| printf '%s' "$APPLE_CODESIGN_CERT" | base64 --decode > "$CERTIFICATE_PATH" | |
| security create-keychain -p "$APPLE_CODESIGN_PASSWORD" "$KEYCHAIN_PATH" | |
| security default-keychain -s "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$APPLE_CODESIGN_PASSWORD" "$KEYCHAIN_PATH" | |
| security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CODESIGN_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_CODESIGN_PASSWORD" "$KEYCHAIN_PATH" | |
| echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" | |
| - name: Package app, ZIP, and DMG | |
| env: | |
| APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} | |
| APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} | |
| APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} | |
| APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| chmod +x packaging/macos/package.sh | |
| packaging/macos/package.sh \ | |
| build \ | |
| install-macos \ | |
| dist-macos \ | |
| "${{ steps.version.outputs.version }}" \ | |
| "$BUILD_TYPE" | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: PollyMC-Continued-macOS | |
| path: | | |
| dist-macos/PollyMC-Continued-${{ steps.version.outputs.version }}-macOS-arm64.zip | |
| dist-macos/PollyMC-Continued-${{ steps.version.outputs.version }}-macOS-arm64.dmg | |
| dist-macos/PollyMC-Continued-${{ steps.version.outputs.version }}-macOS-arm64.sha256 | |
| if-no-files-found: error | |
| - name: Remove temporary signing keychain | |
| if: ${{ always() }} | |
| run: | | |
| set -euo pipefail | |
| if [[ -n "${KEYCHAIN_PATH:-}" && -f "$KEYCHAIN_PATH" ]]; then | |
| security delete-keychain "$KEYCHAIN_PATH" | |
| fi | |
| release: | |
| name: Publish Release | |
| needs: [skip-check, changelog-check, build-windows, build-linux, build-macos] | |
| if: always() && needs.skip-check.outputs.skip != 'true' && needs.changelog-check.result == 'success' && (needs.build-windows.result == 'success' || needs.build-linux.result == 'success' || needs.build-macos.result == 'success') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for skip-release marker | |
| id: skipcheck | |
| run: | | |
| set -euo pipefail | |
| HEAD_MSG=$(git log --format="%s %b" HEAD -1) | |
| echo "DEBUG: HEAD commit message: $HEAD_MSG" | |
| if echo "$HEAD_MSG" | grep -qF '[skip release]'; then | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| echo "Skipping release: [skip release] found in the latest commit." | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| echo "DEBUG: No [skip release] found, proceeding with release." | |
| fi | |
| - name: Download all artifacts | |
| if: steps.skipcheck.outputs.skip != 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| merge-multiple: true | |
| - name: List release files | |
| if: steps.skipcheck.outputs.skip != 'true' | |
| run: | | |
| set -euo pipefail | |
| ls -la PollyMC-Continued-* || true | |
| - name: Prepare release metadata | |
| if: steps.skipcheck.outputs.skip != 'true' | |
| id: prep | |
| run: | | |
| set -euo pipefail | |
| # Version is computed once in skip-check and reused everywhere, | |
| # so this no longer re-derives its own (potentially divergent) | |
| # bump. On a tag push, use the tag itself as-is. | |
| if [[ "${{ github.ref }}" == refs/tags/v* ]]; then | |
| NEW_TAG="${{ github.ref_name }}" | |
| else | |
| NEW_TAG="v${{ needs.skip-check.outputs.version }}" | |
| fi | |
| echo "tag_name=${NEW_TAG}" >> "$GITHUB_OUTPUT" | |
| echo "release_name=PollyMC Continued ${NEW_TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Extract changelog section | |
| if: steps.skipcheck.outputs.skip != 'true' | |
| id: changelog | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ steps.prep.outputs.tag_name }}" | |
| # CHANGELOG.md uses CRLF; strip \r so awk pattern matching is reliable | |
| sed 's/\r$//' CHANGELOG.md > /tmp/changelog_lf.md | |
| awk -v ver="## ${TAG}" ' | |
| $0 == ver { found=1; next } | |
| found && /^## / { exit } | |
| found { print } | |
| ' /tmp/changelog_lf.md > /tmp/section.md | |
| if [[ -s /tmp/section.md ]]; then | |
| # trim leading/trailing blank lines | |
| sed -e '/./,$!d' -e ':a' -e '/^\n*$/{$d;N;ba' -e '}' /tmp/section.md > release_notes.md | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "_No changelog entry found for ${TAG}._" > release_notes.md | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::No CHANGELOG.md section found for ${TAG} - check that the version header matches exactly." | |
| fi | |
| echo "--- release_notes.md ---" | |
| cat release_notes.md | |
| - name: Publish release | |
| if: steps.skipcheck.outputs.skip != 'true' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.prep.outputs.tag_name }} | |
| name: "${{ steps.prep.outputs.release_name }}" | |
| body_path: release_notes.md | |
| generate_release_notes: false | |
| prerelease: ${{ github.ref == 'refs/heads/develop' || github.event_name == 'workflow_dispatch' || contains(github.ref_name, '-rc') }} | |
| files: | | |
| PollyMC-Continued-*-Setup.exe | |
| PollyMC-Continued-*-Windows-portable.zip | |
| PollyMC-Continued-*-Linux-x86_64.AppImage | |
| PollyMC-Continued-*-macOS-arm64.zip | |
| PollyMC-Continued-*-macOS-arm64.dmg | |
| PollyMC-Continued-*-macOS-arm64.sha256 |