Skip to content

Build macOS

Build macOS #19

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'
required: false
type: choice
options:
- arm64
default: arm64
jobs:
build:
runs-on: macos-15 # M1 runner for arm64 support, Xcode 16+
steps:
- name: Checkout repository
uses: actions/checkout@v4
- 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@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
worker/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Download macOS dependencies
run: |
DEPS_VERSION="${{ inputs.deps_tag }}"
DEPS_VERSION="${DEPS_VERSION#deps-v}"
mkdir -p deps/macos-arm64 deps/macos-x64
# Download arm64 deps (zip contains flat structure, no wrapper folder)
if [[ "${{ inputs.arch }}" == "arm64" || "${{ inputs.arch }}" == "both" ]]; then
DEPS_URL="https://github.com/${{ github.repository }}/releases/download/${{ inputs.deps_tag }}/VapourBox-deps-${DEPS_VERSION}-macos-arm64.zip"
echo "Downloading arm64 deps from: $DEPS_URL"
curl -L -o deps-arm64.zip "$DEPS_URL"
unzip -o deps-arm64.zip -d deps/macos-arm64
rm -f deps-arm64.zip
fi
# Download x64 deps (zip contains flat structure, no wrapper folder)
if [[ "${{ inputs.arch }}" == "x64" || "${{ inputs.arch }}" == "both" ]]; then
DEPS_URL="https://github.com/${{ github.repository }}/releases/download/${{ inputs.deps_tag }}/VapourBox-deps-${DEPS_VERSION}-macos-x64.zip"
echo "Downloading x64 deps from: $DEPS_URL"
curl -L -o deps-x64.zip "$DEPS_URL"
unzip -o deps-x64.zip -d deps/macos-x64
rm -f deps-x64.zip
fi
echo "Dependencies extracted:"
ls -la deps/
- 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
# Update deps-version.json
sed -i '' "s/\"version\": \"[^\"]*\"/\"version\": \"${DEPS_VERSION}\"/" app/assets/deps-version.json
sed -i '' "s/\"releaseTag\": \"[^\"]*\"/\"releaseTag\": \"${DEPS_TAG}\"/" app/assets/deps-version.json
# 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 (arm64)
if: ${{ inputs.arch == 'arm64' || inputs.arch == 'both' }}
run: |
cd worker
cargo build --release --target aarch64-apple-darwin
- name: Build Rust worker (x64)
if: ${{ inputs.arch == 'x64' || inputs.arch == 'both' }}
run: |
cd worker
cargo build --release --target x86_64-apple-darwin
- name: Build Flutter app
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)
xcodebuild -workspace Runner.xcworkspace -scheme Pods-Runner \
-configuration Release build ONLY_ACTIVE_ARCH=NO \
-quiet
# Build Runner for arm64 only (must match Podfile ARCHS setting)
xcodebuild -workspace Runner.xcworkspace -scheme Runner \
-configuration Release build ARCHS=arm64 ONLY_ACTIVE_ARCH=YES \
-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 (arm64)
if: ${{ inputs.arch == 'arm64' || inputs.arch == 'both' }}
env:
NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
run: |
./Scripts/package-macos.sh --version "${{ inputs.version }}" --arch arm64 --skip-build --notarize
- name: Package release (x64)
if: ${{ inputs.arch == 'x64' || inputs.arch == 'both' }}
env:
NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
run: |
./Scripts/package-macos.sh --version "${{ inputs.version }}" --arch x64 --skip-build --notarize
- name: Clean up signing keychain
if: always()
run: |
security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" 2>/dev/null || true
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: VapourBox-${{ inputs.version }}-macos
path: dist/VapourBox-${{ inputs.version }}-macos-*.dmg