Build and Package Jumperless App #37
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 and Package Jumperless App | |
| on: | |
| push: | |
| branches: [ main ] | |
| tags: [ 'v*' ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| env: | |
| PYTHON_VERSION: "3.11" | |
| DISABLE_MACOS_DMG: "true" # Flag to disable DMG creation for macOS | |
| DISABLE_MACOS_NOTARIZATION: "false" # Flag to disable notarization for macOS | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Linux builds | |
| - os: ubuntu-22.04 | |
| platform: linux | |
| arch: x64 | |
| artifact-name: "Jumperless-Linux-x64" | |
| executable-name: "Jumperless" | |
| icon-path: "assets/icons/icon.png" | |
| # macOS builds (Intel) | |
| - os: macos-13 | |
| platform: macos | |
| arch: x64 | |
| artifact-name: "Jumperless-macOS-Intel" | |
| executable-name: "Jumperless" | |
| icon-path: "assets/icons/icon.icns" | |
| # macOS builds (Apple Silicon) | |
| - os: macos-latest | |
| platform: macos | |
| arch: arm64 | |
| artifact-name: "Jumperless-macOS-Apple-Silicon" | |
| executable-name: "Jumperless" | |
| icon-path: "assets/icons/icon.icns" | |
| # Windows builds | |
| - os: windows-latest | |
| platform: windows | |
| arch: x64 | |
| artifact-name: "Jumperless-Windows-x64" | |
| executable-name: "Jumperless" | |
| icon-path: "assets/icons/icon.ico" | |
| - os: windows-latest | |
| platform: windows | |
| arch: x86 | |
| artifact-name: "Jumperless-Windows-x86" | |
| executable-name: "Jumperless" | |
| icon-path: "assets/icons/icon.ico" | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python ${{ env.PYTHON_VERSION }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| architecture: ${{ matrix.arch }} | |
| - name: Cache pip dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/pip | |
| ~/.cache/uv | |
| key: ${{ runner.os }}-${{ matrix.arch }}-pip-${{ hashFiles('**/requirements.txt', '**/packagerRequirements.txt') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ matrix.arch }}-pip- | |
| - name: Install uv (modern fast Python package manager) | |
| run: | | |
| python -m pip install --upgrade pip uv | |
| - name: Install system dependencies (Linux) | |
| if: matrix.platform == 'linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y build-essential libfuse2 desktop-file-utils | |
| - name: Install system dependencies (macOS) | |
| if: matrix.platform == 'macos' | |
| run: | | |
| # Only install create-dmg if DMG creation is enabled | |
| if [ "${{ env.DISABLE_MACOS_DMG }}" != "true" ]; then | |
| brew install create-dmg | |
| fi | |
| - name: Install Python dependencies (macOS - Universal binaries) | |
| if: matrix.platform == 'macos' | |
| run: | | |
| # Set environment variables for universal binary builds | |
| export ARCHFLAGS="-arch x86_64 -arch arm64" | |
| export _PYTHON_HOST_PLATFORM="macosx-10.9-universal2" | |
| echo "Installing dependencies with universal binary support..." | |
| # Install pure Python packages first | |
| uv pip install --system beautifulsoup4 packaging requests pyduinocli | |
| # Install packages with C extensions - force rebuild from source for universal binaries | |
| # Note: Using pip directly instead of uv for --no-binary flag | |
| python -m pip install --no-cache-dir --no-binary :all: psutil pyserial | |
| # Install GUI packages (if prebuilt wheels are universal) | |
| uv pip install --system PySide6 websockets | |
| # Install other requirements | |
| uv pip install --system -r PackagingApps/packagerRequirements.txt | |
| - name: Install Python dependencies (Linux) | |
| if: matrix.platform == 'linux' | |
| run: | | |
| uv pip install --system -r PackagingApps/packagerRequirements.txt | |
| uv pip install --system -r requirements.txt | |
| - name: Install Python dependencies (Windows x64) | |
| if: matrix.platform == 'windows' && matrix.arch == 'x64' | |
| run: | | |
| # Use regular pip for Windows - install all dependencies including GUI packages | |
| python -m pip install -r PackagingApps/packagerRequirements.txt | |
| python -m pip install -r requirements.txt | |
| - name: Install Python dependencies (Windows x86) | |
| if: matrix.platform == 'windows' && matrix.arch == 'x86' | |
| run: | | |
| # Windows x86: Skip PySide6 and websockets (not available for 32-bit on Python 3.11+) | |
| # Install PyInstaller and core dependencies first | |
| python -m pip install pyinstaller pyinstaller-hooks-contrib | |
| # Install other non-GUI requirements from packagerRequirements.txt | |
| python -m pip install altgraph beautifulsoup4 certifi charset-normalizer colorama idna packaging psutil pyduinocli pyserial requests setuptools six soupsieve termcolor typing_extensions urllib3 backports-datetime-fromisoformat pywin32 | |
| # Install requirements.txt (basic dependencies) | |
| python -m pip install -r requirements.txt | |
| - name: Verify PyInstaller installation | |
| run: | | |
| python -m PyInstaller --version | |
| - name: Create version info file (Windows) | |
| if: matrix.platform == 'windows' | |
| run: | | |
| python -c " | |
| import sys | |
| sys.path.append('Scripts') | |
| from create_version_info import create_version_info | |
| create_version_info() | |
| " | |
| - name: Build with PyInstaller (Linux, Debian bullseye container for lower glibc) | |
| if: matrix.platform == 'linux' | |
| run: | | |
| echo "Building inside python:3.11-bullseye container (glibc ~2.31)" | |
| docker run --rm -v "$PWD":/work -w /work python:3.11-bullseye bash -lc ' | |
| set -euo pipefail | |
| python -m pip install --upgrade pip | |
| python -m pip install pyinstaller | |
| # Install project dependencies needed for build/runtime | |
| if [ -f PackagingApps/packagerRequirements.txt ]; then python -m pip install -r PackagingApps/packagerRequirements.txt; fi | |
| if [ -f requirements.txt ]; then python -m pip install -r requirements.txt; fi | |
| # Build | |
| python -m PyInstaller --clean --onefile --console \ | |
| --name "Jumperless" \ | |
| --distpath dist/linux \ | |
| --workpath build/linux \ | |
| --icon "assets/icons/icon.png" \ | |
| JumperlessWokwiBridge.py | |
| ' | |
| # Ensure host runner owns build artifacts created by container | |
| sudo chown -R $(id -u):$(id -g) dist build || true | |
| - name: Build with PyInstaller (macOS) | |
| if: matrix.platform == 'macos' | |
| run: | | |
| # Get absolute paths to avoid PyInstaller path issues | |
| CURRENT_DIR=$(pwd) | |
| echo "Current working directory: $CURRENT_DIR" | |
| echo "Contents of current directory:" | |
| ls -la | |
| # Verify dependencies are universal | |
| echo "Verifying universal binaries..." | |
| python -c " | |
| import subprocess, glob, site | |
| site_packages = site.getsitepackages()[0] | |
| binaries = glob.glob(f'{site_packages}/psutil/_psutil_*.so') | |
| for binary in binaries[:1]: # Check at least one | |
| result = subprocess.run(['lipo', '-info', binary], capture_output=True, text=True) | |
| print(f'Checking {binary}') | |
| print(result.stdout) | |
| if 'x86_64' not in result.stdout or 'arm64' not in result.stdout: | |
| print('WARNING: Dependencies may not be universal!') | |
| " | |
| # Use PyInstaller with universal2 target for macOS | |
| echo "Building universal binary (x86_64 + arm64) with PyInstaller" | |
| python -m PyInstaller --clean --windowed --target-arch universal2 --name "Jumperless" --distpath dist/macos --workpath build/macos --icon "assets/icons/icon.icns" JumperlessWokwiBridge.py | |
| # Apply launcher script hack BEFORE code signing and notarization | |
| echo "Setting up macOS launcher script hack BEFORE code signing..." | |
| python Scripts/setup_macos_launcher.py | |
| - name: Code Sign macOS App | |
| if: matrix.platform == 'macos' | |
| env: | |
| MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} | |
| MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| run: | | |
| # Only proceed if certificate is available | |
| if [ -z "$MACOS_CERTIFICATE" ]; then | |
| echo "No signing certificate provided, skipping code signing" | |
| exit 0 | |
| fi | |
| # Create temporary keychain | |
| security create-keychain -p temp_keychain_password temp_keychain | |
| security default-keychain -s temp_keychain | |
| security unlock-keychain -p temp_keychain_password temp_keychain | |
| # Decode and import certificate | |
| echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 | |
| security import certificate.p12 -k temp_keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| # Enable codesign to access the keychain | |
| security set-key-partition-list -S apple-tool:,apple: -s -k temp_keychain_password temp_keychain | |
| # Find the app bundle | |
| APP_PATH="dist/macos/Jumperless.app" | |
| if [ -d "$APP_PATH" ]; then | |
| echo "Signing $APP_PATH" | |
| # Sign the app bundle | |
| codesign --force --sign "Developer ID Application" --deep --options runtime "$APP_PATH" | |
| # Verify the signature | |
| codesign --verify --verbose "$APP_PATH" | |
| echo "Code signing completed successfully" | |
| else | |
| echo "App bundle not found at $APP_PATH" | |
| exit 1 | |
| fi | |
| # Clean up | |
| rm certificate.p12 | |
| security delete-keychain temp_keychain | |
| - name: Notarize macOS App | |
| if: matrix.platform == 'macos' | |
| env: | |
| MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| DISABLE_MACOS_NOTARIZATION: ${{ env.DISABLE_MACOS_NOTARIZATION }} | |
| run: | | |
| # Check if notarization is disabled | |
| if [ "$DISABLE_MACOS_NOTARIZATION" = "true" ]; then | |
| echo "Notarization disabled by DISABLE_MACOS_NOTARIZATION flag" | |
| exit 0 | |
| fi | |
| # Only proceed if both signing certificate and Apple ID are available | |
| if [ -z "$MACOS_CERTIFICATE" ] || [ -z "$APPLE_ID" ]; then | |
| echo "No signing certificate or Apple ID provided, skipping notarization" | |
| exit 0 | |
| fi | |
| # Validate all required secrets are present | |
| if [ -z "$APPLE_ID_PASSWORD" ]; then | |
| echo "ERROR: APPLE_ID_PASSWORD secret is not set" | |
| exit 1 | |
| fi | |
| if [ -z "$APPLE_TEAM_ID" ]; then | |
| echo "ERROR: APPLE_TEAM_ID secret is not set" | |
| exit 1 | |
| fi | |
| # Basic validation of Team ID format (should be 10 characters) | |
| if [ ${#APPLE_TEAM_ID} -ne 10 ]; then | |
| echo "ERROR: APPLE_TEAM_ID should be exactly 10 characters, got: ${#APPLE_TEAM_ID}" | |
| exit 1 | |
| fi | |
| APP_PATH="dist/macos/Jumperless.app" | |
| if [ -d "$APP_PATH" ]; then | |
| echo "Creating archive for notarization" | |
| ditto -c -k --keepParent "$APP_PATH" "Jumperless.zip" | |
| echo "Submitting for notarization" | |
| echo "================================" | |
| echo "Apple ID: $APPLE_ID" | |
| echo "Team ID: $APPLE_TEAM_ID" | |
| echo "Password length: ${#APPLE_ID_PASSWORD} characters" | |
| echo "================================" | |
| xcrun notarytool submit "Jumperless.zip" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_ID_PASSWORD" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --wait | |
| echo "Stapling notarization" | |
| xcrun stapler staple "$APP_PATH" | |
| echo "Notarization completed successfully" | |
| rm "Jumperless.zip" | |
| else | |
| echo "App bundle not found for notarization" | |
| exit 1 | |
| fi | |
| - name: Build with PyInstaller (Windows) | |
| if: matrix.platform == 'windows' | |
| run: | | |
| # Get absolute paths to avoid PyInstaller path issues | |
| $CurrentDir = Get-Location | |
| Write-Host "Current working directory: $CurrentDir" | |
| Write-Host "Contents of current directory:" | |
| Get-ChildItem | |
| # Use PyInstaller with direct command line arguments for consistent builds | |
| Write-Host "Building with PyInstaller" | |
| & python -m PyInstaller --clean --onefile --console --name "Jumperless" --distpath dist/windows --workpath build/windows --icon "assets/icons/icon.ico" JumperlessWokwiBridge.py | |
| shell: pwsh | |
| - name: Create platform-specific package (Linux) | |
| if: matrix.platform == 'linux' | |
| run: | | |
| python Scripts/package_app.py --platform linux --arch ${{ matrix.arch }} | |
| env: | |
| PREFER_TARGZ: "true" # Signal to prefer tar.gz over zip for Linux | |
| - name: Create platform-specific package (macOS) | |
| if: matrix.platform == 'macos' | |
| run: | | |
| python Scripts/package_app.py --platform macos --arch ${{ matrix.arch }} | |
| env: | |
| DISABLE_MACOS_DMG: ${{ env.DISABLE_MACOS_DMG }} | |
| - name: Create platform-specific package (Windows) | |
| if: matrix.platform == 'windows' | |
| run: | | |
| python Scripts/package_app.py --platform windows --arch ${{ matrix.arch }} | |
| - name: Run basic smoke test | |
| run: | | |
| python Scripts/smoke_test.py --platform ${{ matrix.platform }} | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact-name }} | |
| path: | | |
| builds/${{ matrix.platform }}/ | |
| !builds/**/*.pyc | |
| !builds/**/__pycache__/ | |
| retention-days: 90 | |
| compression-level: 6 | |
| - name: Upload to release (if tag) | |
| if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| builds/${{ matrix.platform }}/* | |
| tag_name: "${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('dev-{0}', github.sha) }}" | |
| draft: false | |
| prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Job to create a combined release with all platforms | |
| release: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts/ | |
| - name: Create combined release package | |
| run: | | |
| mkdir -p combined-release | |
| cp -r artifacts/* combined-release/ | |
| # Create combined README | |
| python Scripts/create_combined_readme.py | |
| - name: Upload combined release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: combined-release/**/* | |
| name: "Jumperless ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('dev-{0}', github.sha) }}" | |
| tag_name: "${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('dev-{0}', github.sha) }}" | |
| body: | | |
| ## Jumperless ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || format('Development Build {0}', github.sha) }} | |
| Multi-platform release of the Jumperless Wokwi Bridge application. | |
| ### Downloads | |
| - **Linux x64**: Jumperless-Linux-x64 (tar.gz + Python fallback) | |
| - **macOS Intel**: Jumperless-macOS-Intel (App bundle + Python fallback) | |
| - **macOS Apple Silicon**: Jumperless-macOS-Apple-Silicon (App bundle + Python fallback) | |
| - **Windows x64**: Jumperless-Windows-x64 (EXE + Python fallback) | |
| - **Windows x86**: Jumperless-Windows-x86 (EXE + Python fallback) | |
| ### macOS Users - Important Note | |
| **This release includes properly notarized macOS apps that should run without security warnings.** | |
| **To run the app:** | |
| 1. **Double-click** `Jumperless.app` - Opens Terminal automatically for CLI interaction | |
| 2. **Or run from Terminal** for direct console output: | |
| ```bash | |
| /path/to/Jumperless.app/Contents/MacOS/Jumperless | |
| ``` | |
| **If you still get a security warning:** | |
| 1. Right-click the app and select "Open" to bypass Gatekeeper | |
| 2. Or remove the quarantine attribute: | |
| ```bash | |
| xattr -d com.apple.quarantine Jumperless.app | |
| ``` | |
| ### Installation Methods | |
| Each package includes multiple ways to run Jumperless: | |
| 1. **Native executable** - Double-click to run, no dependencies | |
| 2. **Python script** - Run from source with Python 3.11+ | |
| 3. **Portable launcher** - Cross-platform launcher script | |
| ### Quick Start | |
| 1. Download the package for your platform | |
| 2. Extract/install the package | |
| 3. Run the native executable OR use the Python fallback | |
| See the README.md in each package for detailed instructions. | |
| ### What's New | |
| - **Full macOS notarization support** - Apps are properly signed and notarized | |
| - **No more macOS security warnings** - Apps run without quarantine issues | |
| - Multi-platform CI/CD pipeline with Windows x86 support | |
| - Improved packaging with modern tools | |
| - Better error handling and logging | |
| - Enhanced platform-specific optimizations | |
| - Linux packages now use tar.gz format | |
| - **macOS Terminal launcher** - Double-click opens Terminal automatically | |
| - **Proper launcher script order** - Applied before code signing to maintain notarization | |
| draft: false | |
| prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |