Build Multi-Platform #36
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 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 libgtk-3-dev | |
| - name: Install dependencies | |
| run: flutter pub get | |
| - name: Build Linux | |
| run: flutter build linux --release | |
| - name: Create Linux installation package | |
| run: | | |
| # Set the app name | |
| APP_NAME="scrcpy_gui_prod" | |
| BUILD_PATH="build/linux/x64/release/bundle" | |
| # Create package directory | |
| mkdir -p linux_package | |
| # Copy the entire bundle | |
| cp -r "${BUILD_PATH}" "linux_package/${APP_NAME}" | |
| # Create desktop entry file | |
| cat > linux_package/${APP_NAME}.desktop << 'DESKTOP_EOF' | |
| [Desktop Entry] | |
| Name=Scrcpy GUI | |
| Comment=A GUI for Scrcpy - Android screen mirroring | |
| Exec=scrcpy_gui_prod | |
| Icon=scrcpy_gui_prod | |
| Terminal=false | |
| Type=Application | |
| Categories=Utility; | |
| DESKTOP_EOF | |
| # Create install script | |
| cat > linux_package/install.sh << 'INSTALL_EOF' | |
| #!/bin/bash | |
| # Linux App Installer for scrcpy_gui_prod | |
| # This script installs the app and creates desktop integration | |
| set -e | |
| APP_NAME="scrcpy_gui_prod" | |
| INSTALL_DIR="$HOME/.local/share/${APP_NAME}" | |
| BIN_DIR="$HOME/.local/bin" | |
| DESKTOP_DIR="$HOME/.local/share/applications" | |
| ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps" | |
| echo "======================================" | |
| echo " Linux App Installer" | |
| echo "======================================" | |
| echo "" | |
| # Check if app bundle exists | |
| if [ ! -d "$APP_NAME" ]; then | |
| echo "Error: $APP_NAME directory not found" | |
| echo "Please run this script from the extracted folder" | |
| exit 1 | |
| fi | |
| # Check for required dependencies | |
| echo "Step 1: Checking dependencies..." | |
| if ! dpkg -l | grep -q libgtk-3-0; then | |
| echo "Warning: libgtk-3-0 not found. Installing..." | |
| echo "You may need to enter your password:" | |
| sudo apt-get update | |
| sudo apt-get install -y libgtk-3-0 | |
| fi | |
| echo "✓ Dependencies satisfied" | |
| echo "" | |
| # Create directories | |
| echo "Step 2: Creating directories..." | |
| mkdir -p "$INSTALL_DIR" | |
| mkdir -p "$BIN_DIR" | |
| mkdir -p "$DESKTOP_DIR" | |
| mkdir -p "$ICON_DIR" | |
| echo "✓ Directories created" | |
| echo "" | |
| # Copy application files | |
| echo "Step 3: Installing application..." | |
| if [ -d "$INSTALL_DIR" ]; then | |
| echo "Removing old installation..." | |
| rm -rf "$INSTALL_DIR" | |
| fi | |
| cp -r "$APP_NAME" "$INSTALL_DIR/" | |
| chmod +x "$INSTALL_DIR/${APP_NAME}/${APP_NAME}" | |
| echo "✓ Application installed to $INSTALL_DIR" | |
| echo "" | |
| # Create symlink in bin directory | |
| echo "Step 4: Creating launcher..." | |
| ln -sf "$INSTALL_DIR/${APP_NAME}/${APP_NAME}" "$BIN_DIR/${APP_NAME}" | |
| echo "✓ Launcher created in $BIN_DIR" | |
| echo "" | |
| # Install icon if it exists | |
| echo "Step 5: Installing icon..." | |
| if [ -f "$APP_NAME/data/flutter_assets/icon.png" ]; then | |
| cp "$APP_NAME/data/flutter_assets/icon.png" "$ICON_DIR/${APP_NAME}.png" | |
| echo "✓ Icon installed" | |
| else | |
| echo "⚠ Icon not found, skipping" | |
| fi | |
| echo "" | |
| # Install desktop entry | |
| echo "Step 6: Creating desktop entry..." | |
| cat > "$DESKTOP_DIR/${APP_NAME}.desktop" << DESKTOP_EOF | |
| [Desktop Entry] | |
| Name=Scrcpy GUI | |
| Comment=A GUI for Scrcpy - Android screen mirroring | |
| Exec=$BIN_DIR/${APP_NAME} | |
| Icon=${APP_NAME} | |
| Terminal=false | |
| Type=Application | |
| Categories=Utility; | |
| DESKTOP_EOF | |
| chmod +x "$DESKTOP_DIR/${APP_NAME}.desktop" | |
| echo "✓ Desktop entry created" | |
| echo "" | |
| # Update desktop database | |
| echo "Step 7: Updating desktop database..." | |
| if command -v update-desktop-database &> /dev/null; then | |
| update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true | |
| fi | |
| echo "✓ Database updated" | |
| echo "" | |
| echo "======================================" | |
| echo " Installation Complete!" | |
| echo "======================================" | |
| echo "" | |
| echo "The app has been installed to:" | |
| echo " $INSTALL_DIR" | |
| echo "" | |
| echo "You can now run it by:" | |
| echo " - Searching for 'Scrcpy GUI' in your application menu" | |
| echo " - Running: $APP_NAME" | |
| echo " - Running: $BIN_DIR/${APP_NAME}" | |
| echo "" | |
| echo "To uninstall, run: ./uninstall.sh" | |
| echo "" | |
| INSTALL_EOF | |
| # Make install script executable | |
| chmod +x linux_package/install.sh | |
| # Create uninstall script | |
| cat > linux_package/uninstall.sh << 'UNINSTALL_EOF' | |
| #!/bin/bash | |
| # Uninstaller for scrcpy_gui_prod | |
| APP_NAME="scrcpy_gui_prod" | |
| INSTALL_DIR="$HOME/.local/share/${APP_NAME}" | |
| BIN_DIR="$HOME/.local/bin" | |
| DESKTOP_DIR="$HOME/.local/share/applications" | |
| ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps" | |
| echo "======================================" | |
| echo " Uninstalling Scrcpy GUI" | |
| echo "======================================" | |
| echo "" | |
| # Remove application files | |
| if [ -d "$INSTALL_DIR" ]; then | |
| echo "Removing application files..." | |
| rm -rf "$INSTALL_DIR" | |
| echo "✓ Application files removed" | |
| fi | |
| # Remove symlink | |
| if [ -L "$BIN_DIR/${APP_NAME}" ]; then | |
| echo "Removing launcher..." | |
| rm "$BIN_DIR/${APP_NAME}" | |
| echo "✓ Launcher removed" | |
| fi | |
| # Remove desktop entry | |
| if [ -f "$DESKTOP_DIR/${APP_NAME}.desktop" ]; then | |
| echo "Removing desktop entry..." | |
| rm "$DESKTOP_DIR/${APP_NAME}.desktop" | |
| echo "✓ Desktop entry removed" | |
| fi | |
| # Remove icon | |
| if [ -f "$ICON_DIR/${APP_NAME}.png" ]; then | |
| echo "Removing icon..." | |
| rm "$ICON_DIR/${APP_NAME}.png" | |
| echo "✓ Icon removed" | |
| fi | |
| # Update desktop database | |
| if command -v update-desktop-database &> /dev/null; then | |
| update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true | |
| fi | |
| echo "" | |
| echo "======================================" | |
| echo " Uninstall Complete!" | |
| echo "======================================" | |
| echo "" | |
| UNINSTALL_EOF | |
| # Make uninstall script executable | |
| chmod +x linux_package/uninstall.sh | |
| # Create README | |
| cat > linux_package/README.txt << 'README_EOF' | |
| Linux Installation Instructions | |
| ================================= | |
| EASY INSTALLATION (Recommended): | |
| --------------------------------- | |
| 1. Open Terminal in this directory | |
| 2. Run: chmod +x install.sh | |
| 3. Run: ./install.sh | |
| 4. The app will be installed and available in your application menu! | |
| ALTERNATIVE - One-line installation: | |
| ------------------------------------ | |
| Copy and paste this into Terminal: | |
| chmod +x install.sh && ./install.sh | |
| MANUAL INSTALLATION: | |
| -------------------- | |
| 1. Extract the scrcpy_gui_prod folder to your desired location | |
| 2. Run: chmod +x scrcpy_gui_prod/scrcpy_gui_prod | |
| 3. Run: ./scrcpy_gui_prod/scrcpy_gui_prod | |
| UNINSTALLATION: | |
| --------------- | |
| Run: ./uninstall.sh | |
| REQUIREMENTS: | |
| ------------- | |
| - GTK3 libraries (usually pre-installed) | |
| - If missing, install with: sudo apt-get install libgtk-3-0 | |
| WHAT DOES THE INSTALLER DO? | |
| ---------------------------- | |
| - Installs the app to ~/.local/share/scrcpy_gui_prod | |
| - Creates a launcher in ~/.local/bin/scrcpy_gui_prod | |
| - Adds desktop entry for application menu integration | |
| - Installs the application icon | |
| - Checks and installs required dependencies | |
| TROUBLESHOOTING: | |
| ---------------- | |
| If the app doesn't appear in your menu after installation: | |
| - Log out and log back in | |
| - Or run: update-desktop-database ~/.local/share/applications | |
| If you get permission errors: | |
| - Make sure the install.sh script is executable | |
| - Some distributions may require different installation paths | |
| README_EOF | |
| # Move to artifacts directory | |
| mkdir -p artifacts | |
| mv linux_package artifacts/ | |
| # Show package contents | |
| echo "Package contents:" | |
| ls -lh artifacts/linux_package/ | |
| - 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 |