Skip to content

Build macOS

Build macOS #41

Workflow file for this run

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