Skip to content

Build Multi-Platform #50

Build Multi-Platform

Build Multi-Platform #50

Workflow file for this run

name: Build Multi-Platform
on:
push:
tags:
- 'v*'
pull_request:
branches: [ main ]
schedule:
# 21:00 UTC = 23:00 Europe/Athens — runs on days 1, 5, 9, 13, 17, 21, 25, 29 (every ~4 days)
- cron: '0 21 1,5,9,13,17,21,25,29 * *'
workflow_dispatch:
inputs:
nightly_dry_run:
description: 'Nightly: preview retention/upload actions without changing the release'
required: false
type: boolean
default: false
permissions:
contents: write
jobs:
detect-nightly-changes:
runs-on: ubuntu-latest
outputs:
has_changes: ${{ steps.detect.outputs.has_changes }}
run_builds: ${{ steps.detect.outputs.run_builds }}
steps:
- uses: actions/checkout@v4
- id: detect
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
NIGHTLY_CONTEXT="false"
if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]] || [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${GITHUB_REF}" == "refs/heads/main" ]]; then
NIGHTLY_CONTEXT="true"
fi
if [[ "${NIGHTLY_CONTEXT}" != "true" ]]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "run_builds=true" >> "$GITHUB_OUTPUT"
echo "Non-nightly event; builds will run."
exit 0
fi
CURRENT_SHA="${GITHUB_SHA}"
LAST_SHA=""
if gh release view nightly >/dev/null 2>&1; then
BODY=$(gh release view nightly --json body --jq '.body' || true)
LAST_SHA=$(printf '%s\n' "$BODY" | sed -nE 's/.*Commit: ([0-9a-f]{7,40}).*/\1/p' | head -n1 || true)
fi
if [[ -z "${LAST_SHA}" ]]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "run_builds=true" >> "$GITHUB_OUTPUT"
echo "No recorded nightly commit found; builds will run."
elif [[ "${LAST_SHA}" == "${CURRENT_SHA}" ]]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "run_builds=false" >> "$GITHUB_OUTPUT"
echo "No new commits since last nightly (${CURRENT_SHA}); builds will be skipped."
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "run_builds=true" >> "$GITHUB_OUTPUT"
echo "New commit detected: current=${CURRENT_SHA}, last-nightly=${LAST_SHA}. Builds will run."
fi
nightly-logic-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate nightly versioning and retention logic
run: |
set -euo pipefail
RAW_VERSION=$(grep '^version:' ScrcpyGui/pubspec.yaml | awk '{print $2}')
BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/\+.*$//' | sed -E 's/-.*$//')
TEST_DATE="20260304"
NIGHTLY_VERSION="${BASE_VERSION}-nightly.${TEST_DATE}.1"
[[ "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
[[ "$NIGHTLY_VERSION" =~ ^${BASE_VERSION}-nightly\.[0-9]{8}\.1$ ]]
EXISTING=$(printf '%s\n' \
"${BASE_VERSION}-nightly.20260303.1" \
"${BASE_VERSION}-nightly.20260304.1" \
"${BASE_VERSION}-nightly.20260302.1")
PREVIOUS_VERSION=$(
printf '%s\n' "$EXISTING" \
| grep -v "^${NIGHTLY_VERSION}$" \
| sort -Vr \
| head -n1
)
test "$PREVIOUS_VERSION" = "${BASE_VERSION}-nightly.20260303.1"
# Mirror detect-nightly-changes release-body SHA parsing behavior.
BODY_WITH_SHA="Latest nightly: v${NIGHTLY_VERSION}; Commit: abcdef1234567890"
PARSED_SHA=$(printf '%s\n' "$BODY_WITH_SHA" | sed -nE 's/.*Commit: ([0-9a-f]{7,40}).*/\1/p' | head -n1 || true)
test "$PARSED_SHA" = "abcdef1234567890"
BODY_WITHOUT_SHA="Latest nightly: v${NIGHTLY_VERSION}"
PARSED_EMPTY=$(printf '%s\n' "$BODY_WITHOUT_SHA" | sed -nE 's/.*Commit: ([0-9a-f]{7,40}).*/\1/p' | head -n1 || true)
test -z "$PARSED_EMPTY"
# Skip behavior checks used by detect-nightly-changes.
CURRENT_SHA="abcdef1234567890"
LAST_SHA_SAME="abcdef1234567890"
LAST_SHA_DIFF="1234567abcdef1234"
LAST_SHA_MISSING=""
if [[ -z "${LAST_SHA_MISSING}" ]]; then HAS_CHANGES_MISSING=true; else HAS_CHANGES_MISSING=false; fi
if [[ "${LAST_SHA_SAME}" == "${CURRENT_SHA}" ]]; then HAS_CHANGES_SAME=false; else HAS_CHANGES_SAME=true; fi
if [[ "${LAST_SHA_DIFF}" == "${CURRENT_SHA}" ]]; then HAS_CHANGES_DIFF=false; else HAS_CHANGES_DIFF=true; fi
test "${HAS_CHANGES_MISSING}" = "true"
test "${HAS_CHANGES_SAME}" = "false"
test "${HAS_CHANGES_DIFF}" = "true"
echo "Nightly logic tests passed: base=$BASE_VERSION nightly=$NIGHTLY_VERSION previous=$PREVIOUS_VERSION"
build-windows:
needs: [detect-nightly-changes]
if: needs.detect-nightly-changes.outputs.run_builds == 'true'
runs-on: windows-latest
defaults:
run:
working-directory: ScrcpyGui
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Build Windows
run: flutter build windows --release
- name: Archive Windows build
uses: actions/upload-artifact@v4
with:
name: windows-build
path: ScrcpyGui/build/windows/x64/runner/Release/
build-linux:
needs: [detect-nightly-changes]
if: needs.detect-nightly-changes.outputs.run_builds == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: ScrcpyGui
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Install Linux dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build pkg-config libgtk-3-dev liblzma-dev
- name: Install dependencies
run: flutter pub get
- name: Build Linux
run: flutter build linux --release
- name: Create Linux installation package
run: bash package_linux.sh
- name: Archive Linux build
uses: actions/upload-artifact@v4
with:
name: linux-build
path: ScrcpyGui/artifacts/linux_package/
build-macos:
needs: [detect-nightly-changes]
if: needs.detect-nightly-changes.outputs.run_builds == 'true'
runs-on: macos-latest
defaults:
run:
working-directory: ScrcpyGui
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
cache: true
- name: Install dependencies
run: flutter pub get
- name: Build macOS
run: flutter build macos --release
- name: Fix macOS app permissions and code signing
run: |
# Set the app name
APP_NAME="scrcpy_gui_prod"
APP_PATH="build/macos/Build/Products/Release/${APP_NAME}.app"
# Ensure the main executable has execute permissions
chmod +x "${APP_PATH}/Contents/MacOS/${APP_NAME}"
# Remove resource forks and extended attributes that prevent signing
dot_clean -m "${APP_PATH}"
find "${APP_PATH}" -name "._*" -delete
xattr -cr "${APP_PATH}"
# Sign all frameworks first (don't use --deep on frameworks)
find "${APP_PATH}/Contents/Frameworks" -name "*.framework" -maxdepth 1 | while read framework; do
echo "Signing framework: ${framework}"
/usr/bin/codesign --force --sign - --timestamp=none "${framework}"
done
# Sign the main app bundle
echo "Signing app bundle"
/usr/bin/codesign --force --sign - --timestamp=none "${APP_PATH}"
# Verify the signature
/usr/bin/codesign --verify --verbose "${APP_PATH}"
# Display app info
echo "App signed successfully:"
ls -lh "${APP_PATH}/Contents/MacOS/"
du -sh "${APP_PATH}"
- name: Create DMG and installation package
run: |
# Set the app name
APP_NAME="scrcpy_gui_prod"
APP_PATH="build/macos/Build/Products/Release/${APP_NAME}.app"
DMG_NAME="${APP_NAME}.dmg"
# Create a temporary directory for DMG contents
mkdir -p dmg_temp
# Use ditto instead of cp to preserve all metadata, permissions, and signatures
ditto "${APP_PATH}" "dmg_temp/${APP_NAME}.app"
# Create the DMG with proper flags to preserve code signatures
hdiutil create -volname "${APP_NAME}" -srcfolder dmg_temp -ov -format UDZO "${DMG_NAME}"
# Create package directory for final distribution
mkdir -p macos_package
# Move DMG to package directory
mv "${DMG_NAME}" macos_package/
# Create installation script
cat > macos_package/install.sh << 'SCRIPT_EOF'
#!/bin/bash
# macOS App Installer for scrcpy_gui_prod
# This script removes quarantine flags and installs the app
set -e
APP_NAME="scrcpy_gui_prod"
DMG_FILE="${APP_NAME}.dmg"
INSTALL_DIR="/Applications"
echo "======================================"
echo " macOS App Installer"
echo "======================================"
echo ""
# Check if DMG exists
if [ ! -f "$DMG_FILE" ]; then
echo "Error: $DMG_FILE not found in current directory"
echo "Please run this script from the extracted folder"
exit 1
fi
echo "Step 1: Removing quarantine flags from DMG..."
xattr -cr "$DMG_FILE" 2>/dev/null || true
echo "✓ Quarantine removed"
echo ""
echo "Step 2: Mounting DMG..."
MOUNT_POINT=$(hdiutil attach "$DMG_FILE" | grep Volumes | awk '{print $3}')
if [ -z "$MOUNT_POINT" ]; then
echo "Error: Failed to mount DMG"
exit 1
fi
echo "✓ DMG mounted at: $MOUNT_POINT"
echo ""
echo "Step 3: Removing quarantine from app..."
xattr -cr "$MOUNT_POINT/${APP_NAME}.app" 2>/dev/null || true
echo "✓ App quarantine removed"
echo ""
echo "Step 4: Copying app to Applications folder..."
if [ -d "${INSTALL_DIR}/${APP_NAME}.app" ]; then
echo "Warning: App already exists in Applications. Removing old version..."
rm -rf "${INSTALL_DIR}/${APP_NAME}.app"
fi
cp -R "$MOUNT_POINT/${APP_NAME}.app" "$INSTALL_DIR/"
echo "✓ App copied to $INSTALL_DIR"
echo ""
echo "Step 5: Unmounting DMG..."
hdiutil detach "$MOUNT_POINT" -quiet
echo "✓ DMG unmounted"
echo ""
echo "======================================"
echo " Installation Complete!"
echo "======================================"
echo ""
echo "The app has been installed to:"
echo " $INSTALL_DIR/${APP_NAME}.app"
echo ""
echo "You can now open it from:"
echo " - Spotlight: Press Cmd+Space and type '${APP_NAME}'"
echo " - Applications folder in Finder"
echo " - Or run: open '${INSTALL_DIR}/${APP_NAME}.app'"
echo ""
echo "Opening the app now..."
sleep 1
open "${INSTALL_DIR}/${APP_NAME}.app"
SCRIPT_EOF
# Make the script executable
chmod +x macos_package/install.sh
# Create README
cat > macos_package/README.txt << 'README_EOF'
macOS Installation Instructions
================================
EASY INSTALLATION (Recommended):
---------------------------------
1. Open Terminal (Applications -> Utilities -> Terminal)
2. Type: cd ~/Downloads/scrcpy-gui-macos
(or wherever you extracted this folder)
3. Type: chmod +x install.sh
4. Type: ./install.sh
5. The app will be installed and launched automatically!
ALTERNATIVE - One-line installation:
------------------------------------
Copy and paste this into Terminal:
cd ~/Downloads/scrcpy-gui-macos && chmod +x install.sh && ./install.sh
MANUAL INSTALLATION:
--------------------
1. Open Terminal and run:
xattr -cr scrcpy_gui_prod.dmg
2. Double-click the DMG file
3. Drag the app to your Applications folder
4. Right-click the app -> Open (first time only)
WHY IS THIS NECESSARY?
----------------------
This app is not signed with an Apple Developer ID certificate ($99/year).
macOS adds "quarantine" flags to downloaded files for security.
The install.sh script safely removes these flags.
This is normal for free/open-source macOS apps.
README_EOF
# Move to artifacts directory
mkdir -p artifacts
mv macos_package artifacts/
# Show package contents
echo "Package contents:"
ls -lh artifacts/macos_package/
- name: Archive macOS build
uses: actions/upload-artifact@v4
with:
name: macos-build
path: ScrcpyGui/artifacts/macos_package/
create-release:
needs: [build-windows, build-linux, build-macos]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create ZIP archives
run: |
# Extract version from tag (e.g., refs/tags/v1.0.0 -> 1.0.0)
VERSION=${GITHUB_REF#refs/tags/v}
cd artifacts
# Windows build
zip -r ../scrcpy-gui-windows-v${VERSION}.zip windows-build/
# Linux build
zip -r ../scrcpy-gui-linux-v${VERSION}.zip linux-build/
# macOS build - create ZIP with DMG, install script, and README
cd macos-build
zip -r ../../scrcpy-gui-macos-v${VERSION}.zip .
cd ..
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
scrcpy-gui-windows-v*.zip
scrcpy-gui-linux-v*.zip
scrcpy-gui-macos-v*.zip
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update-nightly-release:
needs: [detect-nightly-changes, build-windows, build-linux, build-macos]
runs-on: ubuntu-latest
if: (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')) && needs.detect-nightly-changes.outputs.has_changes == 'true'
steps:
- uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Compute nightly version
run: |
RAW_VERSION=$(grep '^version:' ScrcpyGui/pubspec.yaml | awk '{print $2}')
BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/\+.*$//' | sed -E 's/-.*$//')
NIGHTLY_VERSION="${BASE_VERSION}-nightly.$(date -u +%Y%m%d).1"
echo "RAW_VERSION=$RAW_VERSION" >> "$GITHUB_ENV"
echo "BASE_VERSION=$BASE_VERSION" >> "$GITHUB_ENV"
echo "NIGHTLY_VERSION=$NIGHTLY_VERSION" >> "$GITHUB_ENV"
- name: Create nightly ZIP archives
run: |
cd artifacts
zip -r ../scrcpy-gui-windows-v${NIGHTLY_VERSION}.zip windows-build/
zip -r ../scrcpy-gui-linux-v${NIGHTLY_VERSION}.zip linux-build/
cd macos-build
zip -r ../../scrcpy-gui-macos-v${NIGHTLY_VERSION}.zip .
- name: Ensure nightly pre-release exists
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if ! gh release view nightly >/dev/null 2>&1; then
gh release create nightly \
--title "Nightly Builds" \
--notes "Automated nightly pre-release builds. Assets keep only the current nightly and one previous nightly." \
--prerelease
fi
- name: Keep only current + previous nightly versions, then upload current
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NIGHTLY_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.nightly_dry_run || false }}
run: |
set -euo pipefail
# Discover existing nightly versions from asset names:
# scrcpy-gui-<platform>-v<version>.zip
mapfile -t EXISTING_VERSIONS < <(
gh release view nightly --json assets --jq '.assets[].name' \
| sed -nE 's/^scrcpy-gui-(windows|linux|macos)-v(.+)\.zip$/\2/p' \
| sort -Vu
)
PREVIOUS_VERSION=""
if [ ${#EXISTING_VERSIONS[@]} -gt 0 ]; then
PREVIOUS_VERSION=$(
printf '%s\n' "${EXISTING_VERSIONS[@]}" \
| grep -v "^${NIGHTLY_VERSION}$" \
| sort -Vr \
| head -n1 || true
)
fi
echo "Nightly dry run: ${NIGHTLY_DRY_RUN}"
echo "Current nightly version: ${NIGHTLY_VERSION}"
echo "Previous nightly version: ${PREVIOUS_VERSION:-none}"
# Remove any nightly asset that is neither current nor previous
mapfile -t EXISTING_ASSETS < <(gh release view nightly --json assets --jq '.assets[].name')
for asset in "${EXISTING_ASSETS[@]}"; do
if [[ "$asset" =~ ^scrcpy-gui-(windows|linux|macos)-v(.+)\.zip$ ]]; then
asset_version="${BASH_REMATCH[2]}"
if [[ "$asset_version" != "$NIGHTLY_VERSION" && "$asset_version" != "$PREVIOUS_VERSION" ]]; then
if [[ "${NIGHTLY_DRY_RUN}" == "true" ]]; then
echo "[DRY RUN] Would delete asset: $asset"
else
gh release delete-asset nightly "$asset" --yes || true
fi
fi
fi
done
# Upload current nightly assets (idempotent for same-day reruns)
if [[ "${NIGHTLY_DRY_RUN}" == "true" ]]; then
echo "[DRY RUN] Would upload: scrcpy-gui-windows-v${NIGHTLY_VERSION}.zip"
echo "[DRY RUN] Would upload: scrcpy-gui-linux-v${NIGHTLY_VERSION}.zip"
echo "[DRY RUN] Would upload: scrcpy-gui-macos-v${NIGHTLY_VERSION}.zip"
else
gh release upload nightly "scrcpy-gui-windows-v${NIGHTLY_VERSION}.zip" --clobber
gh release upload nightly "scrcpy-gui-linux-v${NIGHTLY_VERSION}.zip" --clobber
gh release upload nightly "scrcpy-gui-macos-v${NIGHTLY_VERSION}.zip" --clobber
fi
# Update release metadata each run for clarity
CURRENT_BODY=$(gh release view nightly --json body --jq '.body' || true)
STATIC_SECTION=$(printf '%s' "$CURRENT_BODY" | tr -d '\r' | awk '/^---$/{found=1; next} found{print}' || true)
DYNAMIC_NOTES=$(printf 'Latest nightly: v%s\nPrevious nightly: %s\nSource branch: main\nCommit: %s' \
"${NIGHTLY_VERSION}" "${PREVIOUS_VERSION:-none}" "${GITHUB_SHA}")
if [[ -n "$STATIC_SECTION" ]]; then
RELEASE_NOTES=$(printf '%s\n\n---\n%s' "$DYNAMIC_NOTES" "$STATIC_SECTION")
else
RELEASE_NOTES="${DYNAMIC_NOTES}"
fi
if [[ "${NIGHTLY_DRY_RUN}" == "true" ]]; then
echo "[DRY RUN] Would edit release title/notes for nightly"
echo "[DRY RUN] Notes would be:"
printf '%s\n' "$RELEASE_NOTES"
else
gh release edit nightly \
--title "Nightly Builds (v${NIGHTLY_VERSION})" \
--notes "$RELEASE_NOTES"
fi