Build macOS #41
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 macOS | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version number (e.g., 0.2.0)' | |
| required: true | |
| type: string | |
| deps_tag: | |
| description: 'Deps release tag (e.g., deps-v1.0.0)' | |
| required: true | |
| type: string | |
| arch: | |
| description: 'Architecture (both = two separate per-arch DMGs; or build a single arch)' | |
| required: false | |
| type: choice | |
| options: | |
| - both | |
| - arm64 | |
| - x64 | |
| default: both | |
| notarize: | |
| description: 'Notarize with Apple (needs valid developer agreements; off = signed-only, fine for local testing)' | |
| required: false | |
| type: boolean | |
| default: true | |
| jobs: | |
| # Expand the arch choice into a matrix the build job fans out over. | |
| setup: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.set.outputs.matrix }} | |
| steps: | |
| - id: set | |
| run: | | |
| case "${{ inputs.arch }}" in | |
| arm64) echo 'matrix=["arm64"]' >> "$GITHUB_OUTPUT" ;; | |
| x64) echo 'matrix=["x64"]' >> "$GITHUB_OUTPUT" ;; | |
| both) echo 'matrix=["arm64","x64"]' >> "$GITHUB_OUTPUT" ;; | |
| *) echo 'matrix=["arm64","x64"]' >> "$GITHUB_OUTPUT" ;; | |
| esac | |
| build: | |
| needs: setup | |
| runs-on: macos-15 # Apple Silicon; x64 cross-compiles via ARCHS=x86_64 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: ${{ fromJSON(needs.setup.outputs.matrix) }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v5 | |
| # Resolve the Xcode ARCHS value and the per-arch macOS deployment target. | |
| # x64/Intel targets 12.0 (Monterey, issue #39) — its bundled deps are | |
| # source-built to load there. arm64 targets 15.0 to match its minos-15 | |
| # prebuilt deps (it has no old hardware floor and no old runner). The | |
| # target flows to rustc (worker) and xcodebuild/CocoaPods (app) below. | |
| - name: Resolve arch variables | |
| id: arch | |
| run: | | |
| case "${{ matrix.arch }}" in | |
| arm64) echo "xcode_archs=arm64" >> "$GITHUB_OUTPUT"; echo "deploy_target=15.0" >> "$GITHUB_OUTPUT" ;; | |
| x64) echo "xcode_archs=x86_64" >> "$GITHUB_OUTPUT"; echo "deploy_target=12.0" >> "$GITHUB_OUTPUT" ;; | |
| esac | |
| - name: Setup Flutter | |
| uses: subosito/flutter-action@v2 | |
| with: | |
| channel: 'stable' | |
| cache: true | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin,x86_64-apple-darwin | |
| - name: Cache Rust dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/bin/ | |
| ~/.cargo/registry/index/ | |
| ~/.cargo/registry/cache/ | |
| ~/.cargo/git/db/ | |
| worker/target/ | |
| key: ${{ runner.os }}-cargo-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-${{ matrix.arch }}- | |
| ${{ runner.os }}-cargo- | |
| # Per-arch deps are downloaded at runtime by the app (not bundled); this | |
| # validates the matching zip exists for the arch being built. | |
| - name: Download macOS dependencies (${{ matrix.arch }}) | |
| run: | | |
| DEPS_VERSION="${{ inputs.deps_tag }}" | |
| DEPS_VERSION="${DEPS_VERSION#deps-v}" | |
| mkdir -p deps/macos-${{ matrix.arch }} | |
| DEPS_URL="https://github.com/${{ github.repository }}/releases/download/${{ inputs.deps_tag }}/VapourBox-deps-${DEPS_VERSION}-macos-${{ matrix.arch }}.zip" | |
| echo "Downloading ${{ matrix.arch }} deps from: $DEPS_URL" | |
| curl -L -o deps.zip "$DEPS_URL" | |
| unzip -o deps.zip -d deps/macos-${{ matrix.arch }} | |
| rm -f deps.zip | |
| - name: Setup CocoaPods | |
| run: gem install cocoapods | |
| - name: Update version numbers | |
| run: | | |
| VERSION="${{ inputs.version }}" | |
| DEPS_TAG="${{ inputs.deps_tag }}" | |
| DEPS_VERSION="${DEPS_TAG#deps-v}" | |
| # Update pubspec.yaml | |
| sed -i '' "s/^version: .*/version: ${VERSION}+1/" app/pubspec.yaml | |
| # deps-version.json is committed and per-platform-versioned — leave as-is. | |
| # Update macOS Info.plist | |
| /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" app/macos/Runner/Info.plist | |
| /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" app/macos/Runner/Info.plist | |
| # Update Cargo.toml | |
| sed -i '' "s/^version = \".*\"/version = \"${VERSION}\"/" worker/Cargo.toml | |
| - name: Build Rust worker (${{ matrix.arch }}) | |
| env: | |
| # rustc stamps the Mach-O minos from this (per-arch: arm64 15, x64 12). | |
| MACOSX_DEPLOYMENT_TARGET: ${{ steps.arch.outputs.deploy_target }} | |
| run: | | |
| cd worker | |
| case "${{ matrix.arch }}" in | |
| arm64) | |
| cargo build --release --target aarch64-apple-darwin ;; | |
| x64) | |
| cargo build --release --target x86_64-apple-darwin ;; | |
| esac | |
| - name: Build Flutter app (${{ matrix.arch }}) | |
| env: | |
| # Drives app/macos/Podfile to build Pods for the target arch and target. | |
| VAPOURBOX_ARCHS: ${{ steps.arch.outputs.xcode_archs }} | |
| VAPOURBOX_DEPLOYMENT_TARGET: ${{ steps.arch.outputs.deploy_target }} | |
| MACOSX_DEPLOYMENT_TARGET: ${{ steps.arch.outputs.deploy_target }} | |
| run: | | |
| cd app | |
| flutter pub get | |
| dart run build_runner build --delete-conflicting-outputs | |
| # Try flutter build first — it generates ephemeral files needed by xcodebuild. | |
| # It may fail with module dependency errors, which is OK. | |
| flutter build macos --release || true | |
| # Two-step xcodebuild — handles module dependency errors that flutter build can't | |
| cd macos | |
| rm -rf Pods Podfile.lock | |
| pod install | |
| # Build Pods-Runner scheme first (all CocoaPods dependencies). | |
| # MACOSX_DEPLOYMENT_TARGET overrides the xcodeproj's 12.0 baseline so | |
| # the arm64 slice is stamped 15.0 (x64 stays 12.0). | |
| xcodebuild -workspace Runner.xcworkspace -scheme Pods-Runner \ | |
| -configuration Release build ONLY_ACTIVE_ARCH=NO \ | |
| ARCHS="${{ steps.arch.outputs.xcode_archs }}" \ | |
| MACOSX_DEPLOYMENT_TARGET="${{ steps.arch.outputs.deploy_target }}" \ | |
| -quiet | |
| # Build Runner for the target arch (must match Podfile ARCHS setting) | |
| xcodebuild -workspace Runner.xcworkspace -scheme Runner \ | |
| -configuration Release build ONLY_ACTIVE_ARCH=NO \ | |
| ARCHS="${{ steps.arch.outputs.xcode_archs }}" \ | |
| MACOSX_DEPLOYMENT_TARGET="${{ steps.arch.outputs.deploy_target }}" \ | |
| -quiet | |
| # Copy to Flutter build location | |
| DERIVED_APP=$(find ~/Library/Developer/Xcode/DerivedData/Runner-*/Build/Products/Release/vapourbox.app -maxdepth 0 2>/dev/null | head -1) | |
| if [ -z "$DERIVED_APP" ]; then | |
| echo "ERROR: Could not find built app in DerivedData" | |
| exit 1 | |
| fi | |
| mkdir -p ../build/macos/Build/Products/Release | |
| cp -R "$DERIVED_APP" ../build/macos/Build/Products/Release/ | |
| - name: Import Developer ID certificate | |
| env: | |
| MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} | |
| MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} | |
| run: | | |
| KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" | |
| CERT_PATH="$RUNNER_TEMP/developer-id.p12" | |
| echo "$MACOS_CERTIFICATE" | base64 --decode > "$CERT_PATH" | |
| # Create a dedicated, unlocked keychain just for this build | |
| security create-keychain -p "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Import the Developer ID Application cert + private key | |
| security import "$CERT_PATH" -P "$MACOS_CERTIFICATE_PASSWORD" \ | |
| -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" | |
| # Allow codesign to use the key without an interactive prompt | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: \ | |
| -s -k "$MACOS_KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Make this keychain the default + only one in the search list so | |
| # package-macos.sh's `security find-identity` resolves the identity | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" | |
| security default-keychain -s "$KEYCHAIN_PATH" | |
| rm -f "$CERT_PATH" | |
| echo "Signing identities available:" | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" | |
| - name: Package release (${{ matrix.arch }}) | |
| env: | |
| NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }} | |
| NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }} | |
| NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }} | |
| run: | | |
| ARGS=(--version "${{ inputs.version }}" --arch ${{ matrix.arch }} --skip-build) | |
| if [ "${{ inputs.notarize }}" = "true" ]; then ARGS+=(--notarize); fi | |
| ./Scripts/package-macos.sh "${ARGS[@]}" | |
| - name: Clean up signing keychain | |
| if: always() | |
| run: | | |
| security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: VapourBox-${{ inputs.version }}-macos-${{ matrix.arch }} | |
| path: dist/VapourBox-${{ inputs.version }}-macos-${{ matrix.arch }}.dmg |