Files
plezy/.github/workflows/build.yml
T
edde746 ae001d9ff3 fix(linux): restore VAAPI hardware decode and AV1 software fallback in the bundled libmpv
Hardware decoding stopped working for Linux users on 2.13.0 (Fedora 44
report): every source decodes in software, and AV1 plays black video with
audio. Two defects in the pinned libmpv build.

First, mpv's meson 'drm' feature silently disabled itself because the CI
builder lacks libdisplay-info, and every VAAPI path that does not depend
on a display server is derived from it: vaapi-copy's standalone render-node
device (the path 2.12.1 worked on) and the GL dmabuf interop for direct
vaapi. With only the Wayland VA provider compiled in, a machine whose
Wayland VA display fails to initialize has no fallback, and vaapi-copy
has an empty provider list - every source lands on software decoding.
Pin -Ddrm=enabled, -Dvaapi-drm=enabled, -Degl=enabled and
-Dvaapi-wayland=enabled, and add libdisplay-info-dev to the CI package
lists, so a missing piece fails the build instead of shipping silent
software decode.

Second, the bundled static FFmpeg has no AV1 software decoder: its native
av1 codec is hardware-accelerated only, so once hwdec fails there is no AV1
path at all - every packet errors, video hits EOF, the plane goes black
while audio keeps playing. Pin dav1d 1.5.4 (both VideoLAN remotes agree on
the tag object and root commit), build it static before ffmpeg, and pass
--enable-libdav1d.

The build-plan stub test now asserts the hwdec feature flags, the dav1d
static build, and ffmpeg's libdav1d. Verified in an ubuntu:24.04 container
with the production flag sets: meson reports drm, vaapi-drm, vaapi-wayland,
egl and dmabuf-interop-gl enabled, and ffmpeg configures CONFIG_LIBDAV1D=yes
with the AV1 VAAPI hwaccel.

close #1874
2026-08-14 21:49:41 +02:00

1048 lines
42 KiB
YAML

name: Build
on:
workflow_dispatch:
inputs:
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.44.0"
# 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
build/windows/arm64/mpv-dev-arm64
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: Install 7-Zip
if: matrix.arch == 'arm64' && steps.windows-native-cache.outputs.cache-hit != 'true'
shell: pwsh
run: choco install 7zip -y
- 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 build
id: libmpv-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: libmpv-prefix
key: ${{ env.TRUSTED_BUILD_CACHE_VERSION }}-libmpv-${{ runner.arch }}-${{ hashFiles('linux/packaging/build-libmpv.sh', 'linux/packaging/native-inputs.json') }}
- name: Build libmpv
if: steps.libmpv-cache.outputs.cache-hit != 'true'
shell: bash
run: bash linux/packaging/build-libmpv.sh
- 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 }}
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: 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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}