Files
plezy/.github/workflows/build.yml
T
edde746 200d896bea build: switch every platform's libmpv supply chain to the unified mpv-build repo
Plezy consumed mpv through four unrelated supply chains: an MPVKit fork
via SwiftPM for the Apple platforms, a libmpv-android fork's AAR for
Android, an in-CI from-source build for Linux, and an unpinned
sourceforge mpv-dev 7z for Windows. All four now consume the same
per-commit, content-addressed binaries from
https://github.com/edde746/mpv-build, pinned to one commit and built
from one set of pinned sources (mpv v0.41.0 on Apple/Android/Windows,
ffmpeg n8.0.1, our libass fork).

- Apple: the SwiftPM package moves from edde746/MPVKit to
  edde746/mpv-build across the ios/macos/tvos projects;
  scripts/set_mpvkit_revision.sh becomes set_native_revision.sh, writes
  every pin site plus the new repo-root mpv-build.lock.json, and
  tvos/scripts/wire_mpv.rb derives the package repo from the locks.
- Android: the mpv Kotlin API and JNI glue move in-app under
  android/libmpv (repackaged com.edde746.plezy.libmpv, exports renamed,
  shrinker rules covered), and the module downloads per-ABI native
  tarballs (lib/*.so incl. libc++_shared.so + include/) driven entirely
  by mpv-build.lock.json, with a PLEZY_LOCAL_MPV_DIR escape hatch for
  locally built artifacts. The fork AAR and its Maven coordinates are
  gone.
- Linux: CI downloads the prebuilt self-relocating libmpv prefix
  (lock-driven, sha256-verified) instead of compiling mpv/ffmpeg/dav1d/
  libplacebo/shaderc from source; linux/packaging/build-libmpv.sh and
  its test are deleted and native-inputs.json shrinks to the simdutf
  entry the CMake builds still fetch.
- Windows: both arches FetchContent the mpv-build dev zips with
  URL_HASH enforcement, replacing the checksum-less sourceforge
  download and its ARM64 7-Zip special case; guard scripts updated.

The lock plus the Apple pin sites all point at mpv-build commit
d7c3d559, whose manifest was verified asset-by-asset against the
published release digests (17/17 match). Verified locally: wire and
pin-script suites green, runtime-input checks green, all four Android
ABI tarballs downloaded/verified/extracted through the real Gradle
tasks, the Windows FetchContent block exercised end to end through
cmake, the Linux asset hash and layout checked against the workflow
contract, and xcodebuild resolved the flipped SwiftPM graph with
SwiftPM validating every binary checksum.
2026-09-02 11:31:47 +02:00

1096 lines
44 KiB
YAML

name: Build
run-name: ${{ inputs.release_tag != '' && format('Release {0}', inputs.release_tag) || format('Build {0}', github.sha) }}
on:
workflow_dispatch:
inputs:
release_tag:
description: Tag for a draft release; omit to build Actions artifacts only
default: ''
type: string
build_android:
description: Build Android
default: true
type: boolean
build_ios:
description: Build iOS
default: true
type: boolean
build_macos:
description: Build macOS
default: true
type: boolean
build_windows:
description: Build Windows
default: true
type: boolean
build_linux:
description: Build Linux
default: true
type: boolean
env:
# Only place this workflow names the SDK; .github/actions/setup-flutter-git pins the same release.
FLUTTER_VERSION: "3.47.1"
# Shared by every release build command; SENTRY_DIST stays per-platform.
RELEASE_DART_DEFINES: --dart-define=ENABLE_UPDATE_CHECK=true ${{ github.repository == 'edde746/plezy' && '--dart-define=ENABLE_SENTRY=true' || '' }} --dart-define=GIT_COMMIT=${{ github.sha }} --dart-define=SENTRY_ENVIRONMENT=github --dart-define=ENABLE_DONATIONS=true
TRUSTED_BUILD_CACHE_VERSION: trusted-build-v1
LINUX_APT_PACKAGES: >
clang cmake meson ninja-build pkg-config nasm libgtk-3-dev libevdev-dev liblzma-dev
libstdc++-12-dev libasound2-dev libass-dev libfreetype-dev libfontconfig-dev libfribidi-dev
libharfbuzz-dev libepoxy-dev libegl-dev libgl-dev libgnutls28-dev libpipewire-0.3-dev
libva-dev libvdpau-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxi-dev
libxss-dev libxpresent-dev libxkbcommon-dev libpulse-dev libdbus-1-dev libdrm-dev
libdisplay-info-dev libgbm-dev libwayland-dev wayland-protocols liblcms2-dev libmujs-dev liblua5.2-dev
ruby ruby-dev rubygems build-essential rpm libarchive-tools imagemagick libcurl4-openssl-dev
jobs:
validate-trusted-ref:
name: Validate trusted build ref
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Require the protected main branch
shell: bash
run: |
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "Release builds may only run from refs/heads/main." >&2
exit 1
fi
build-android:
needs: validate-trusted-ref
if: ${{ inputs.build_android }}
runs-on: ubuntu-latest
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Setup Java
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
with:
distribution: "temurin"
java-version: "21"
- name: Setup Flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: "${{ env.TRUSTED_BUILD_CACHE_VERSION }}-flutter-:os:-:channel:-:version:-:arch:-:hash:"
pub-cache: false
- name: Cache Pub dependencies
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.pub-cache
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-${{ runner.os }}-${{ runner.arch }}-pub-${{ hashFiles('pubspec.yaml', 'pubspec.lock') }}
- name: Cache Gradle
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Install dependencies
run: flutter pub get --enforce-lockfile --no-example
- name: Configure Android signing
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/upload-keystore.jks
cat > android/key.properties << EOF
storePassword=${{ secrets.ANDROID_STORE_PASSWORD }}
keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
storeFile=upload-keystore.jks
EOF
- name: Build APKs
run: flutter build apk --release --split-per-abi ${{ env.RELEASE_DART_DEFINES }} --dart-define=SENTRY_DIST=github-android-apk --obfuscate --split-debug-info=debug-info/android-apk --extra-gen-snapshot-options=--save-obfuscation-map=debug-info/android-apk/obfuscation.map.json
- name: Upload symbols to bugs.plezy.app
if: github.repository == 'edde746/plezy'
env:
BUGS_ADMIN_TOKEN: ${{ secrets.BUGS_ADMIN_TOKEN }}
SENTRY_DIST: github-android-apk
run: ./scripts/upload-symbols.sh android-apk
- name: Clean up keystore
if: always()
run: rm -f android/app/upload-keystore.jks android/key.properties
- name: Package APKs
run: |
tar -czf plezy-android-arm64-v8a.tar.gz -C build/app/outputs/flutter-apk app-arm64-v8a-release.apk --transform 's/app-arm64-v8a-release.apk/plezy.apk/'
tar -czf plezy-android-armeabi-v7a.tar.gz -C build/app/outputs/flutter-apk app-armeabi-v7a-release.apk --transform 's/app-armeabi-v7a-release.apk/plezy.apk/'
tar -czf plezy-android-x86_64.tar.gz -C build/app/outputs/flutter-apk app-x86_64-release.apk --transform 's/app-x86_64-release.apk/plezy.apk/'
- name: Attest APKs
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-path: |
plezy-android-arm64-v8a.tar.gz
plezy-android-armeabi-v7a.tar.gz
plezy-android-x86_64.tar.gz
- name: Upload APKs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: android-apk
path: |
plezy-android-arm64-v8a.tar.gz
plezy-android-armeabi-v7a.tar.gz
plezy-android-x86_64.tar.gz
build-ios:
needs: validate-trusted-ref
if: ${{ inputs.build_ios }}
runs-on: macos-26
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Setup Flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: "${{ env.TRUSTED_BUILD_CACHE_VERSION }}-flutter-:os:-:channel:-:version:-:arch:-:hash:"
pub-cache: false
- name: Cache Pub dependencies
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.pub-cache
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-${{ runner.os }}-${{ runner.arch }}-pub-${{ hashFiles('pubspec.yaml', 'pubspec.lock') }}
- name: Cache CocoaPods
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-${{ runner.os }}-ios-pods-${{ hashFiles('**/Podfile.lock') }}
- name: Install dependencies
run: flutter pub get --enforce-lockfile --no-example
- name: Build iOS (no codesign)
run: flutter build ios --release --no-codesign ${{ env.RELEASE_DART_DEFINES }} --dart-define=SENTRY_DIST=github-ios --split-debug-info=debug-info/ios
- name: Upload symbols to bugs.plezy.app
if: github.repository == 'edde746/plezy'
env:
BUGS_ADMIN_TOKEN: ${{ secrets.BUGS_ADMIN_TOKEN }}
SENTRY_DIST: github-ios
run: ./scripts/upload-symbols.sh ios
- name: Create IPA
run: |
mkdir -p Payload
cp -r build/ios/iphoneos/Runner.app Payload/
zip -r plezy-ios.ipa Payload
- name: Attest IPA
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-path: plezy-ios.ipa
- name: Upload IPA
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ios-ipa
path: plezy-ios.ipa
build-macos:
needs: validate-trusted-ref
if: ${{ inputs.build_macos }}
runs-on: macos-26
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Setup Flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: "${{ env.TRUSTED_BUILD_CACHE_VERSION }}-flutter-:os:-:channel:-:version:-:arch:-:hash:"
pub-cache: false
- name: Cache Pub dependencies
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.pub-cache
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-${{ runner.os }}-${{ runner.arch }}-pub-${{ hashFiles('pubspec.yaml', 'pubspec.lock') }}
- name: Cache CocoaPods
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
macos/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-${{ runner.os }}-macos-pods-${{ hashFiles('**/Podfile.lock') }}
- name: Install dependencies
run: flutter pub get --enforce-lockfile --no-example
- name: Build macOS
run: flutter build macos --release ${{ env.RELEASE_DART_DEFINES }} --dart-define=SENTRY_DIST=github-macos --split-debug-info=debug-info/macos
- name: Upload symbols to bugs.plezy.app
if: github.repository == 'edde746/plezy'
env:
BUGS_ADMIN_TOKEN: ${{ secrets.BUGS_ADMIN_TOKEN }}
SENTRY_DIST: github-macos
run: ./scripts/upload-symbols.sh macos
- name: Import Code Signing Certificate
env:
MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
echo "$MACOS_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
security import $CERTIFICATE_PATH -k $KEYCHAIN_PATH -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Add keychain to search list
security list-keychains -d user -s $KEYCHAIN_PATH login.keychain
- name: Sign Application
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
# Find the identity
IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '".*"' | tr -d '"')
echo "Signing with identity: $IDENTITY"
APP_PATH="build/macos/Build/Products/Release/Plezy.app"
# Deep sign everything inside-out: executables, dylibs, XPC services, app bundles, frameworks
find "$APP_PATH/Contents/Frameworks" \( -name "*.xpc" -o -name "*.app" -o -name "*.framework" -o -name "*.dylib" \) -print0 \
| sort -zr \
| while IFS= read -r -d '' item; do
echo "Signing: $item"
codesign --force --sign "$IDENTITY" --timestamp --options runtime "$item"
done
# Sign any remaining bare executables (e.g. Sparkle's Autoupdate)
find "$APP_PATH/Contents/Frameworks" -type f -perm +111 ! -name "*.dylib" ! -path "*/Headers/*" ! -path "*/Modules/*" -print0 \
| while IFS= read -r -d '' exe; do
codesign --force --sign "$IDENTITY" --timestamp --options runtime "$exe" 2>/dev/null || true
done
# Sign the app bundle itself
echo "Signing app bundle: $APP_PATH"
codesign --force --sign "$IDENTITY" --timestamp --options runtime --entitlements macos/Runner/Release.entitlements "$APP_PATH"
- name: Verify Signature
run: |
APP_PATH="build/macos/Build/Products/Release/Plezy.app"
echo "Verifying signature..."
codesign -dvvv "$APP_PATH"
echo "Verifying signed entitlements..."
codesign -d --entitlements :- "$APP_PATH" > "$RUNNER_TEMP/macos-entitlements.plist"
/usr/libexec/PlistBuddy -c "Print" "$RUNNER_TEMP/macos-entitlements.plist"
/usr/libexec/PlistBuddy -c "Print :com.apple.security.cs.allow-jit" "$RUNNER_TEMP/macos-entitlements.plist"
/usr/libexec/PlistBuddy -c "Print :com.apple.security.cs.allow-unsigned-executable-memory" "$RUNNER_TEMP/macos-entitlements.plist"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
- name: Notarize Application
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
APP_PATH="build/macos/Build/Products/Release/Plezy.app"
# Create a temporary ZIP for notarization submission
echo "Creating ZIP for notarization..."
ditto -c -k --keepParent "$APP_PATH" notarization.zip
# Submit for notarization
echo "Submitting to Apple's notary service..."
xcrun notarytool submit notarization.zip \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
# Staple the notarization ticket to the app
echo "Stapling notarization ticket..."
xcrun stapler staple "$APP_PATH"
# Verify stapling
echo "Verifying notarization..."
xcrun stapler validate "$APP_PATH"
- name: Create DMG
run: |
mkdir -p dmg-staging
cp -R build/macos/Build/Products/Release/Plezy.app dmg-staging/
ln -s /Applications dmg-staging/Applications
hdiutil create -format ULMO -srcfolder dmg-staging -volname "Plezy" plezy-macos.dmg
- name: Sign DMG for Sparkle (EdDSA)
if: env.SPARKLE_PRIVATE_KEY != ''
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
printf -- "-----BEGIN ED25519 PRIVATE KEY-----\n%s\n-----END ED25519 PRIVATE KEY-----\n" "$SPARKLE_PRIVATE_KEY" > ed25519_key.pem
OUTPUT=$(dart run auto_updater:sign_update plezy-macos.dmg ed25519_key.pem)
rm ed25519_key.pem
# Extract edSignature value from output like: sparkle:edSignature="..." length="..."
SIGNATURE=$(echo "$OUTPUT" | grep -o 'edSignature="[^"]*"' | sed 's/edSignature="//;s/"//')
echo "MACOS_ED_SIGNATURE=$SIGNATURE" >> $GITHUB_ENV
echo "MACOS_DMG_SIZE=$(stat -f%z plezy-macos.dmg)" >> $GITHUB_ENV
- name: Attest macOS DMG
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-path: plezy-macos.dmg
- name: Write macOS signature metadata
run: |
echo "${{ env.MACOS_ED_SIGNATURE }}" > macos-ed-signature.txt
echo "${{ env.MACOS_DMG_SIZE }}" > macos-dmg-size.txt
- name: Upload macOS DMG
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: macos-dmg
path: |
plezy-macos.dmg
macos-ed-signature.txt
macos-dmg-size.txt
build-windows:
needs: validate-trusted-ref
name: Build Windows (${{ matrix.arch }})
if: ${{ inputs.build_windows }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: windows-latest
flutter_setup: action
native_cache_path: build/windows/x64/_deps
- arch: arm64
runner: windows-11-arm
flutter_setup: git
native_cache_path: build/windows/arm64/_deps
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Cache Windows native dependencies
id: windows-native-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: ${{ matrix.native_cache_path }}
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-windows-native-${{ matrix.arch }}-${{ hashFiles('windows/CMakeLists.txt') }}
- name: Setup Flutter
if: matrix.flutter_setup == 'action'
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: "${{ env.TRUSTED_BUILD_CACHE_VERSION }}-flutter-:os:-:channel:-:version:-:arch:-:hash:"
pub-cache: false
- name: Set up Flutter from its pinned commit
if: matrix.flutter_setup == 'git'
uses: ./.github/actions/setup-flutter-git
- name: Cache Pub dependencies
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~\AppData\Local\Pub\Cache
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-${{ runner.os }}-${{ runner.arch }}-pub-${{ hashFiles('pubspec.yaml', 'pubspec.lock') }}
- name: Install dependencies
shell: pwsh
run: flutter pub get --enforce-lockfile --no-example
- name: Install patched Flutter engine (DComp)
shell: pwsh
run: |
flutter precache --windows
.\windows\tool\install-patched-engine.ps1
- name: Build Windows ${{ matrix.arch }}
shell: pwsh
run: flutter build windows --release ${{ env.RELEASE_DART_DEFINES }} --dart-define=SENTRY_DIST=github-windows-${{ matrix.arch }} --split-debug-info=debug-info/windows-${{ matrix.arch }}
- name: Verify Windows bundle contents
shell: pwsh
run: |
$bundleDir = "build/windows/${{ matrix.arch }}/runner/Release"
if (-not (Test-Path -LiteralPath $bundleDir -PathType Container)) {
throw "Windows bundle directory does not exist: $bundleDir"
}
$bundlePath = (Resolve-Path -LiteralPath $bundleDir).Path
$foundFiles = @(
Get-ChildItem -LiteralPath $bundlePath -File -Recurse |
ForEach-Object {
$_.FullName.Substring($bundlePath.Length + 1).Replace('\', '/')
} |
Sort-Object
)
Write-Host "Files found in ${bundleDir}:"
$foundFiles | ForEach-Object { Write-Host " $_" }
$requiredFiles = @(
"plezy.exe"
"flutter_windows.dll"
"sqlite3.dll"
"libmpv-2.dll"
"data/app.so"
"data/icudtl.dat"
"data/flutter_assets/NativeAssetsManifest.json"
# MSVC runtime bundled by InstallRequiredSystemLibraries in
# windows/CMakeLists.txt; without it the app crashes at startup on
# machines whose system-wide redist is older than the CI toolset.
"msvcp140.dll"
"vcruntime140.dll"
)
$forbiddenFiles = @(
"plezy.lib"
"plezy.exp"
"simdutf.lib"
)
$bundleErrors = @()
foreach ($file in $requiredFiles) {
if ($foundFiles -notcontains $file) {
Write-Output "::error::Required Windows bundle file is missing: $file"
$bundleErrors += "missing $file"
}
}
foreach ($file in $forbiddenFiles) {
if ($foundFiles -contains $file) {
Write-Output "::error::Forbidden link by-product is present in the Windows bundle: $file"
$bundleErrors += "present $file"
}
}
# sentry.dll is the inproc-backend native SDK; crashpad_handler.exe is
# intentionally absent (windows/CMakeLists.txt sets
# SENTRY_NATIVE_BACKEND=inproc), so do not assert on it here.
if ($bundleErrors.Count -gt 0) {
throw "Windows bundle verification failed: $($bundleErrors -join '; ')"
}
- name: Upload symbols to bugs.plezy.app
if: github.repository == 'edde746/plezy'
shell: pwsh
env:
BUGS_ADMIN_TOKEN: ${{ secrets.BUGS_ADMIN_TOKEN }}
SENTRY_DIST: github-windows-${{ matrix.arch }}
run: .\scripts\upload-symbols.ps1 windows-${{ matrix.arch }}
- name: Upload ${{ matrix.arch }} build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-${{ matrix.arch }}-build
path: build/windows/${{ matrix.arch }}/runner/Release/
package-windows:
needs: build-windows
if: ${{ inputs.build_windows }}
runs-on: windows-latest
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Setup Flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: "${{ env.TRUSTED_BUILD_CACHE_VERSION }}-flutter-:os:-:channel:-:version:-:arch:-:hash:"
pub-cache: false
- name: Install dependencies
shell: pwsh
run: flutter pub get --enforce-lockfile --no-example
- name: Download x64 build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-x64-build
path: build-x64
- name: Download arm64 build
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-arm64-build
path: build-arm64
- name: Read version from pubspec
id: version
shell: pwsh
run: |
$v = (Select-String -Path pubspec.yaml -Pattern '^version:\s*(\S+)').Matches[0].Groups[1].Value -replace '\+.*'
echo "version=$v" >> $env:GITHUB_OUTPUT
- name: Build installer and portables
run: .\windows\build-installer.ps1 -X64BuildDir "build-x64" -Arm64BuildDir "build-arm64" -Version "${{ steps.version.outputs.version }}"
# Unsigned on purpose: the Store re-signs the bundle during
# certification. See windows/build-msix.ps1.
- name: Build Store package (MSIX)
run: .\windows\build-msix.ps1 -X64BuildDir "build-x64" -Arm64BuildDir "build-arm64" -Version "${{ steps.version.outputs.version }}"
- name: Sign installer for WinSparkle (EdDSA)
if: env.SPARKLE_PRIVATE_KEY != ''
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
shell: pwsh
run: |
$keyPath = Join-Path $env:RUNNER_TEMP "plezy-winsparkle-ed25519.pem"
try {
Set-Content -Path $keyPath -Value $env:SPARKLE_PRIVATE_KEY -Encoding ascii -NoNewline
$output = & dart run auto_updater:sign_update plezy-windows-installer.exe $keyPath
if ($LASTEXITCODE -ne 0) {
throw "WinSparkle signer failed with exit code $LASTEXITCODE"
}
$match = [regex]::Match($output, 'edSignature="([^"]*)"')
if (-not $match.Success) {
throw "WinSparkle signer returned no EdDSA signature"
}
Set-Content -Path win-ed-signature.txt -Value $match.Groups[1].Value -Encoding ascii -NoNewline
Set-Content -Path win-installer-size.txt -Value (Get-Item plezy-windows-installer.exe).Length.ToString() -Encoding ascii -NoNewline
} finally {
Remove-Item -Path $keyPath -Force -ErrorAction SilentlyContinue
}
- name: Attest Windows artifacts
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-path: |
plezy-windows-x64-portable.7z
plezy-windows-arm64-portable.7z
plezy-windows-installer.exe
plezy-windows.msixbundle
- name: Upload x64 portable
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-x64-portable
path: plezy-windows-x64-portable.7z
- name: Upload arm64 portable
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-arm64-portable
path: plezy-windows-arm64-portable.7z
- name: Upload installer
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-installer
path: |
plezy-windows-installer.exe
win-ed-signature.txt
win-installer-size.txt
# Deliberately not attached to the GitHub release in create-release: a
# Store-identity package cannot be installed without the Store's
# certificate, so it is only useful as a Partner Center upload.
- name: Upload Store package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: windows-msix
path: plezy-windows.msixbundle
build-linux:
needs: validate-trusted-ref
name: Build Linux (${{ matrix.arch }})
if: ${{ inputs.build_linux }}
runs-on: ${{ matrix.runner }}
permissions:
id-token: write
attestations: write
contents: read
strategy:
fail-fast: false
matrix:
include:
- arch: x64
runner: ubuntu-latest
flutter_channel: stable
pkg_config_arch: x86_64-linux-gnu
- arch: arm64
runner: ubuntu-24.04-arm
flutter_channel: master
pkg_config_arch: aarch64-linux-gnu
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Setup Flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: ${{ matrix.flutter_channel }}
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
cache-key: "${{ env.TRUSTED_BUILD_CACHE_VERSION }}-flutter-:os:-:channel:-:version:-:arch:-:hash:"
pub-cache: false
- name: Cache Pub dependencies
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.pub-cache
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-${{ runner.os }}-${{ runner.arch }}-pub-${{ hashFiles('pubspec.yaml', 'pubspec.lock') }}
- name: Cache APT packages
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # latest
with:
packages: ${{ env.LINUX_APT_PACKAGES }}
version: trusted-build-v1.1
- name: Install Linux dependencies
shell: bash
run: |
read -r -a packages <<< "$LINUX_APT_PACKAGES"
for attempt in 1 2 3; do
if sudo apt-get update -o Acquire::Retries=3 && sudo apt-get install -y -o Acquire::Retries=3 "${packages[@]}"; then
exit 0
fi
sudo apt-get clean
sudo rm -rf /var/lib/apt/lists/*
sleep $((attempt * 15))
done
exit 1
- name: Cache libmpv prefix
id: libmpv-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: libmpv-prefix
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-libmpv-${{ runner.arch }}-${{ hashFiles('mpv-build.lock.json') }}
# The prebuilt prefix from our unified mpv-build repo: the lock names the
# asset and the expected SHA-256, so the bytes are verified before a
# single one is extracted. The tarball is a self-relocating prefix tree
# (lib/ or lib/<triplet>/ with the libmpv.so* chain, lib/libshaderc_shared.so*,
# include/, and pkgconfig/ dirs with ${pcfiledir}-relative roots), so the
# downstream PKG_CONFIG_PATH, bundle-copy, and find steps are unchanged.
- name: Fetch libmpv
if: steps.libmpv-cache.outputs.cache-hit != 'true'
shell: bash
run: |
command -v zstd >/dev/null 2>&1 || { sudo apt-get update && sudo apt-get install -y --no-install-recommends zstd; }
python3 - <<'PY'
import hashlib
import json
import platform
import subprocess
import tempfile
import urllib.request
from pathlib import Path
lock = json.loads(Path("mpv-build.lock.json").read_text(encoding="utf-8"))
group = lock["artifacts"]["linux"]
entry = group["assets"][platform.machine()]
url = f"{group['assetBase']}/{entry['asset']}"
print(f"fetching {url}", flush=True)
with urllib.request.urlopen(url) as response:
data = response.read()
digest = hashlib.sha256(data).hexdigest()
if digest != entry["checksum"]:
raise SystemExit(f"{entry['asset']}: SHA-256 {digest} does not match locked {entry['checksum']}")
Path("libmpv-prefix").mkdir(exist_ok=True)
with tempfile.NamedTemporaryFile(suffix=".tar.zst") as archive:
archive.write(data)
archive.flush()
subprocess.run(["tar", "--zstd", "-xf", archive.name, "-C", "libmpv-prefix"], check=True)
PY
- name: Install fpm
shell: bash
run: sudo gem install fpm --version 1.17.0 --no-document
- name: Install dependencies
shell: bash
run: flutter pub get --enforce-lockfile --no-example
- name: Build Linux ${{ matrix.arch }}
shell: bash
run: flutter build linux --release ${{ env.RELEASE_DART_DEFINES }} --dart-define=SENTRY_DIST=github-linux-${{ matrix.arch }} --split-debug-info=debug-info/linux-${{ matrix.arch }}
env:
PKG_CONFIG_PATH: ${{ github.workspace }}/libmpv-prefix/lib/pkgconfig:${{ github.workspace }}/libmpv-prefix/lib/${{ matrix.pkg_config_arch }}/pkgconfig
- name: Upload symbols to bugs.plezy.app
if: github.repository == 'edde746/plezy'
shell: bash
env:
BUGS_ADMIN_TOKEN: ${{ secrets.BUGS_ADMIN_TOKEN }}
SENTRY_DIST: github-linux-${{ matrix.arch }}
run: ./scripts/upload-symbols.sh linux-${{ matrix.arch }}
- name: Resolve bundle directory
shell: bash
run: |
if [[ "${{ matrix.arch }}" == "x64" ]]; then
bundle_dir=build/linux/x64/release/bundle
else
bundle_dir=$(find build/linux -path "*/release/bundle" -type d | head -1)
fi
if [[ -z "$bundle_dir" || ! -d "$bundle_dir" ]]; then
echo "ERROR: Linux bundle directory not found" >&2
exit 1
fi
echo "BUNDLE_DIR=$bundle_dir" >> "$GITHUB_ENV"
# Everything below resolves the bundle *before* packaging, so the packages
# and the tarball are cut from one identical tree.
#
# The plane needs the pinned Wayland-enabled libmpv: a distro libmpv still
# plays and still does HDR, but silently drops hwdec to vaapi-copy - which
# was measured, not assumed. So libmpv travels with us and no artifact
# depends on a host one. That removed the host `mpv` dependency, and with it
# the transitive pull of everything libmpv itself needs - libass, pulse,
# pipewire, fontconfig and the rest. bundle-libs.sh is what supplies those,
# so it has to run before packaging too, or the packages would carry libmpv
# and nothing it links.
- name: Copy libmpv into bundle
shell: bash
run: |
BUNDLE_LIB="$BUNDLE_DIR/lib"
LIBMPV_DIR=$(dirname "$(find libmpv-prefix -name 'libmpv.so' | head -1)")
cp -a "$LIBMPV_DIR"/libmpv.so* "$BUNDLE_LIB/"
cp -a libmpv-prefix/lib/libshaderc_shared.so* "$BUNDLE_LIB/"
- name: Bundle shared libraries
shell: bash
run: bash linux/packaging/bundle-libs.sh "$BUNDLE_DIR"
- name: Copy wrapper script into bundle
shell: bash
run: cp linux/packaging/plezy.sh "$BUNDLE_DIR/plezy.sh"
# Same check the package smoke build runs, against the artifact that
# actually ships: a library nobody declares is a broken install, and it is
# invisible until a user on a clean machine tries to launch.
#
# This also stands in for the `ldd ./plezy | grep "not found"` step that
# used to run after packaging. That one folded ldd's stderr into grep's
# input and dropped its exit status, so ldd failing outright - a missing
# loader, an exec-format mismatch, no ldd at all - left the match empty and
# printed "All dependencies resolved." This guard runs ldd over every
# object under lib/ as well as the executable, fails on an unresolved
# soname, fails when ldd cannot read an object, and refuses to pass when
# the walk found no host libraries at all. Packaging below only reads the
# bundle, so a second shell ldd afterwards could only restate a weaker
# subset of what this already proved about the very same tree.
- name: Verify every unbundled library the bundle needs is declared
shell: bash
run: python3 linux/packaging/check-bundle-host-deps.py "$BUNDLE_DIR"
# Last, from the fully resolved tree above. The host-dependency guard is
# skipped because the named step above just ran it against this same
# bundle; the internal run exists for by-hand packaging outside CI.
- name: Build Linux Packages
shell: bash
run: |
BUILD_DIR="$BUNDLE_DIR" \
ARCH_SUFFIX=${{ matrix.arch }} \
OUTPUT_DIR="$GITHUB_WORKSPACE" \
PLEZY_SKIP_HOST_DEP_CHECK=1 \
python3 linux/packaging/build-packages.py
# The depends lists reached fpm above; only the packages it wrote can show
# they arrived. Same script the smoke build runs, so the two jobs cannot
# drift on what counts as declared - and unlike the smoke build, this one
# covers arm64 and the artifacts users actually install.
- name: Verify the declared dependencies reached the package metadata
shell: bash
run: python3 linux/packaging/check-package-deps.py "$GITHUB_WORKSPACE" --arch ${{ matrix.arch }}
- name: Create tarball
shell: bash
run: |
cd "$BUNDLE_DIR"
tar -czf "$GITHUB_WORKSPACE/plezy-linux-${{ matrix.arch }}.tar.gz" *
- name: Attest Linux ${{ matrix.arch }} artifacts
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4
with:
subject-path: |
plezy-linux-${{ matrix.arch }}.tar.gz
plezy-linux-${{ matrix.arch }}.deb
plezy-linux-${{ matrix.arch }}.rpm
plezy-linux-${{ matrix.arch }}.pkg.tar.zst
- name: Upload Linux ${{ matrix.arch }} artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: linux-${{ matrix.arch }}
path: |
plezy-linux-${{ matrix.arch }}.tar.gz
plezy-linux-${{ matrix.arch }}.deb
plezy-linux-${{ matrix.arch }}.rpm
plezy-linux-${{ matrix.arch }}.pkg.tar.zst
create-release:
needs: [validate-trusted-ref, build-android, build-ios, build-macos, build-windows, package-windows, build-linux]
if: ${{ always() && needs.validate-trusted-ref.result == 'success' && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && inputs.build_android && inputs.build_ios && inputs.build_macos && inputs.build_windows && inputs.build_linux && inputs.release_tag != '' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
sparse-checkout: pubspec.yaml
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Read version from pubspec.yaml
id: version
run: |
VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//')
BUILD_NUMBER=$(grep '^version:' pubspec.yaml | sed 's/.*+//')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT
- name: Validate release tag
if: ${{ inputs.release_tag != '' }}
env:
RELEASE_TAG: ${{ inputs.release_tag }}
VERSION: ${{ steps.version.outputs.version }}
run: |
if [[ ! "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Release tag must use semantic versioning: $RELEASE_TAG" >&2
exit 1
fi
if [[ "$RELEASE_TAG" != "$VERSION" ]]; then
echo "Release tag $RELEASE_TAG does not match pubspec version $VERSION." >&2
exit 1
fi
- name: Download Android artifacts
if: ${{ inputs.build_android }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: android-apk
path: artifacts/android-apk
- name: Download iOS artifact
if: ${{ inputs.build_ios }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ios-ipa
path: artifacts/ios-ipa
- name: Download macOS artifact
if: ${{ inputs.build_macos }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: macos-dmg
path: artifacts/macos-dmg
- name: Download Windows x64 artifact
if: ${{ inputs.build_windows }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-x64-portable
path: artifacts/windows-x64-portable
- name: Download Windows arm64 artifact
if: ${{ inputs.build_windows }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-arm64-portable
path: artifacts/windows-arm64-portable
- name: Download Windows installer artifact
if: ${{ inputs.build_windows }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: windows-installer
path: artifacts/windows-installer
- name: Download Linux x64 artifacts
if: ${{ inputs.build_linux }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: linux-x64
path: artifacts/linux-x64
- name: Download Linux arm64 artifacts
if: ${{ inputs.build_linux }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: linux-arm64
path: artifacts/linux-arm64
- name: Display structure of downloaded files
run: ls -R artifacts
- name: Generate appcast.xml
run: |
VERSION="${{ steps.version.outputs.version }}"
BUILD_NUMBER="${{ steps.version.outputs.build_number }}"
# Read macOS signature metadata (may be empty if secrets not configured)
MACOS_ED_SIG=""
MACOS_DMG_SIZE="0"
if [ -f artifacts/macos-dmg/macos-ed-signature.txt ]; then
MACOS_ED_SIG=$(cat artifacts/macos-dmg/macos-ed-signature.txt)
fi
if [ -f artifacts/macos-dmg/macos-dmg-size.txt ]; then
MACOS_DMG_SIZE=$(cat artifacts/macos-dmg/macos-dmg-size.txt)
fi
# Read Windows signature metadata (may be empty if secrets not configured)
WIN_ED_SIG=""
WIN_INSTALLER_SIZE="0"
if [ -f artifacts/windows-installer/win-ed-signature.txt ]; then
WIN_ED_SIG=$(cat artifacts/windows-installer/win-ed-signature.txt)
fi
if [ -f artifacts/windows-installer/win-installer-size.txt ]; then
WIN_INSTALLER_SIZE=$(cat artifacts/windows-installer/win-installer-size.txt)
fi
cat > appcast.xml << XMLEOF
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Plezy Updates</title>
<item>
<title>Version ${VERSION}</title>
<sparkle:version>${BUILD_NUMBER}</sparkle:version>
<sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
<sparkle:releaseNotesLink>https://gh-html.edde746.dev/edde746/plezy/${VERSION}</sparkle:releaseNotesLink>
XMLEOF
# macOS enclosure
if [ -n "$MACOS_ED_SIG" ]; then
cat >> appcast.xml << XMLEOF
<enclosure url="https://github.com/edde746/plezy/releases/download/${VERSION}/plezy-macos.dmg"
length="${MACOS_DMG_SIZE}" type="application/octet-stream"
sparkle:edSignature="${MACOS_ED_SIG}"
sparkle:os="macos" />
XMLEOF
fi
# Windows enclosure
if [ -n "$WIN_ED_SIG" ]; then
cat >> appcast.xml << XMLEOF
<enclosure url="https://github.com/edde746/plezy/releases/download/${VERSION}/plezy-windows-installer.exe"
length="${WIN_INSTALLER_SIZE}" type="application/octet-stream"
sparkle:edSignature="${WIN_ED_SIG}"
sparkle:installerArguments="/SILENT /SP-"
sparkle:os="windows" />
XMLEOF
fi
cat >> appcast.xml << 'XMLEOF'
</item>
</channel>
</rss>
XMLEOF
# Clean up leading whitespace from heredoc indentation
sed -i 's/^ //' appcast.xml
echo "Generated appcast.xml:"
cat appcast.xml
- name: Collect release files
id: release-files
run: |
{
echo 'files<<EOF'
if [ "${{ inputs.build_android }}" = "true" ]; then
echo artifacts/android-apk/plezy-android-arm64-v8a.tar.gz
echo artifacts/android-apk/plezy-android-armeabi-v7a.tar.gz
echo artifacts/android-apk/plezy-android-x86_64.tar.gz
fi
if [ "${{ inputs.build_ios }}" = "true" ]; then
echo artifacts/ios-ipa/plezy-ios.ipa
fi
if [ "${{ inputs.build_macos }}" = "true" ]; then
echo artifacts/macos-dmg/plezy-macos.dmg
fi
if [ "${{ inputs.build_windows }}" = "true" ]; then
echo artifacts/windows-x64-portable/plezy-windows-x64-portable.7z
echo artifacts/windows-arm64-portable/plezy-windows-arm64-portable.7z
echo artifacts/windows-installer/plezy-windows-installer.exe
fi
if [ "${{ inputs.build_linux }}" = "true" ]; then
echo 'artifacts/linux-x64/*'
echo 'artifacts/linux-arm64/*'
fi
echo appcast.xml
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Create Release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
files: ${{ steps.release-files.outputs.files }}
draft: true
prerelease: false
name: ${{ inputs.release_tag }}
tag_name: ${{ inputs.release_tag }}
target_commitish: ${{ github.sha }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}