#!/usr/bin/env bash # scripts/build-installer-mac.sh # # End-to-end macOS DMG installer build pipeline for DragonMoonlight. # Produces a distributable .dmg that end-users can open, drag the app to /Applications, and launch. # # Usage: # bash scripts/build-installer-mac.sh [OPTIONS] # # Options: # --version VERSION App version string (default: read from git tag, fallback "0.1.0") # --sign IDENTITY Apple Developer codesigning identity (optional; skips signing if omitted) # --notarize Submit to Apple Notary Service after signing (requires --sign + env vars) # --universal Build boringtun as universal fat binary (arm64 + x86_64) # --build-dir DIR CMake build directory (default: build/mac) # --output-dir DIR Where to place final .dmg (default: dist/) # # Environment variables (required if --notarize): # NOTARIZE_APPLE_ID Apple ID email for notarization # NOTARIZE_TEAM_ID Apple Team ID for notarization # NOTARIZE_PASSWORD Apple app-specific password for notarization # # Prerequisites: # - cmake # - cargo (Rust toolchain) # - macdeployqt (ships with Qt) # - create-dmg (brew install create-dmg) # - codesign (part of Xcode CLI tools) # - rsvg-convert (optional; brew install librsvg; used to render SVG background) set -euo pipefail # Platform check: require macOS [[ "$(uname)" != "Darwin" ]] && { echo "[ERROR] This script requires macOS" >&2; exit 1; } # Color codes for output readonly COLOR_RESET='\033[0m' readonly COLOR_INFO='\033[0;36m' # Cyan readonly COLOR_OK='\033[0;32m' # Green readonly COLOR_ERROR='\033[0;31m' # Red # Helper functions info() { echo -e "${COLOR_INFO}[INFO]${COLOR_RESET} $*" } ok() { echo -e "${COLOR_OK}[OK]${COLOR_RESET} $*" } error() { echo -e "${COLOR_ERROR}[ERROR]${COLOR_RESET} $*" >&2 } # Build success flag for cleanup control BUILD_SUCCESS=false cleanup() { info "Cleaning up temporary files..." if [[ "$BUILD_SUCCESS" == false && -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" fi } # Trap EXIT for cleanup, ERR for early error exit trap cleanup EXIT trap 'exit 1' ERR # ============================================================================= # Parse CLI arguments # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(dirname "$SCRIPT_DIR")" VERSION="" SIGN_IDENTITY="" NOTARIZE=false UNIVERSAL=false BUILD_DIR="build/mac" OUTPUT_DIR="dist" while [[ $# -gt 0 ]]; do case "$1" in --version) VERSION="$2" shift 2 ;; --sign) SIGN_IDENTITY="$2" shift 2 ;; --notarize) NOTARIZE=true shift ;; --universal) UNIVERSAL=true shift ;; --build-dir) BUILD_DIR="$2" shift 2 ;; --output-dir) OUTPUT_DIR="$2" shift 2 ;; *) error "Unknown option: $1" exit 1 ;; esac done # Convert relative paths to absolute if [[ ! "$BUILD_DIR" = /* ]]; then BUILD_DIR="$REPO_ROOT/$BUILD_DIR" fi if [[ ! "$OUTPUT_DIR" = /* ]]; then OUTPUT_DIR="$REPO_ROOT/$OUTPUT_DIR" fi # ============================================================================= # Determine version # ============================================================================= if [[ -z "$VERSION" ]]; then info "Determining version from git tag..." if VERSION=$(cd "$REPO_ROOT" && git describe --tags 2>/dev/null | sed 's/^v//'); then ok "Version from git: $VERSION" else VERSION="0.1.0" info "No git tag found; using fallback version: $VERSION" fi fi TEMP_DIR="$(mktemp -d)" info "Using temporary directory: $TEMP_DIR" # ============================================================================= # Prerequisites check # ============================================================================= info "Checking prerequisites..." for cmd in cmake cargo macdeployqt codesign; do if ! command -v "$cmd" &>/dev/null; then error "Required tool not found: $cmd" if [[ "$cmd" == "macdeployqt" ]]; then error " macdeployqt ships with Qt. Install Qt from https://www.qt.io/download or use Homebrew" fi exit 1 fi done if ! command -v create-dmg &>/dev/null; then error "Required tool not found: create-dmg" error " Install via: brew install create-dmg" exit 1 fi ok "All prerequisites found" if $NOTARIZE && [[ -z "$SIGN_IDENTITY" ]]; then error "--notarize requires --sign" exit 1 fi if $NOTARIZE; then for var in NOTARIZE_APPLE_ID NOTARIZE_TEAM_ID NOTARIZE_PASSWORD; do if [[ -z "${!var:-}" ]]; then error "--notarize requires environment variable: $var" exit 1 fi done fi # ============================================================================= # Step 1: Build boringtun # ============================================================================= info "Step 1: Building boringtun..." BORINGTUN_ARGS="" if $UNIVERSAL; then BORINGTUN_ARGS="--universal" fi bash "$SCRIPT_DIR/build-boringtun.sh" $BORINGTUN_ARGS ok "boringtun build complete" # ============================================================================= # Step 2: CMake configure # ============================================================================= info "Step 2: Configuring CMake..." mkdir -p "$BUILD_DIR" CMAKE_ARGS=( "-S" "$REPO_ROOT" "-B" "$BUILD_DIR" "-DCMAKE_BUILD_TYPE=Release" ) if $UNIVERSAL; then CMAKE_ARGS+=("-DCMAKE_OSX_ARCHITECTURES=arm64;x86_64") fi cmake "${CMAKE_ARGS[@]}" ok "CMake configuration complete" # ============================================================================= # Step 3: Build # ============================================================================= info "Step 3: Building DragonMoonlight..." NUM_JOBS=$(sysctl -n hw.logicalcpu) cmake --build "$BUILD_DIR" --config Release -j"$NUM_JOBS" ok "Build complete" # ============================================================================= # Step 4: Locate app bundle # ============================================================================= info "Step 4: Locating app bundle..." APP_BUNDLE=$(find "$BUILD_DIR" -name "DragonMoonlight.app" -type d | head -1) if [[ -z "$APP_BUNDLE" ]]; then error "DragonMoonlight.app not found in $BUILD_DIR" exit 1 fi ok "Found: $APP_BUNDLE" # ============================================================================= # Step 5: macdeployqt # ============================================================================= info "Step 5: Running macdeployqt to bundle Qt frameworks..." # Use macdeployqt without -dmg (we'll create the DMG ourselves) # Use absolute path for -qmldir macdeployqt "$APP_BUNDLE" -qmldir="$REPO_ROOT/app" ok "macdeployqt complete" # ============================================================================= # Step 6: Code signing (if requested) # ============================================================================= if [[ -n "$SIGN_IDENTITY" ]]; then info "Step 6: Code signing with identity: $SIGN_IDENTITY..." codesign --force --deep --options runtime \ --entitlements "$REPO_ROOT/package/mac/entitlements.plist" \ --sign "$SIGN_IDENTITY" \ "$APP_BUNDLE" ok "Code signing complete" else info "Step 6: Skipping code signing (--sign not provided)" fi # ============================================================================= # Step 7: Notarization (if requested) # ============================================================================= if $NOTARIZE; then info "Step 7: Preparing for notarization..." NOTARIZE_ZIP="$TEMP_DIR/DragonMoonlight.zip" cd "$(dirname "$APP_BUNDLE")" ditto -c -k --sequesterRsrc "$(basename "$APP_BUNDLE")" "$NOTARIZE_ZIP" info "Submitting to Apple Notary Service..." xcrun notarytool submit "$NOTARIZE_ZIP" \ --apple-id "$NOTARIZE_APPLE_ID" \ --team-id "$NOTARIZE_TEAM_ID" \ --password "$NOTARIZE_PASSWORD" \ --wait info "Stapling notarization ticket..." xcrun stapler staple "$APP_BUNDLE" ok "Notarization complete" else info "Step 7: Skipping notarization (--notarize not provided)" fi # ============================================================================= # Step 8: Create DMG # ============================================================================= info "Step 8: Creating DMG installer..." mkdir -p "$OUTPUT_DIR" # Prepare background image DMG_BG_PATH="$REPO_ROOT/package/mac/dmg-background.png" CREATE_DMG_ARGS=( --volname "DragonMoonlight" --window-pos 200 120 --window-size 660 400 --icon-size 128 --icon "DragonMoonlight.app" 160 185 --hide-extension "DragonMoonlight.app" --app-drop-link 500 185 ) # Handle SVG background conversion if [[ -f "$REPO_ROOT/package/mac/dmg-background.svg" ]]; then info "Converting SVG background to PNG..." if command -v rsvg-convert &>/dev/null; then rsvg-convert -w 660 -h 400 "$REPO_ROOT/package/mac/dmg-background.svg" -o "$DMG_BG_PATH" CREATE_DMG_ARGS+=(--background "$DMG_BG_PATH") ok "SVG converted to PNG" else info "rsvg-convert not found — DMG will use plain background (brew install librsvg to add branding)" fi fi # Check for icon and conditionally add --volicon if [[ -f "$APP_BUNDLE/Contents/Resources/icon.icns" ]]; then CREATE_DMG_ARGS+=(--volicon "$APP_BUNDLE/Contents/Resources/icon.icns") else info "Warning: icon.icns not found; DMG will not have custom volume icon" fi # Use create-dmg DMG_OUTPUT="$OUTPUT_DIR/DragonMoonlight-$VERSION.dmg" rm -f "$DMG_OUTPUT" create-dmg "${CREATE_DMG_ARGS[@]}" "$DMG_OUTPUT" "$(dirname "$APP_BUNDLE")/DragonMoonlight.app" # Validate DMG is non-zero if [[ ! -s "$DMG_OUTPUT" ]]; then error "DMG is empty or missing: $DMG_OUTPUT" exit 1 fi ok "DMG created: $DMG_OUTPUT" # ============================================================================= # Step 9: Print checksum # ============================================================================= info "Computing SHA256 checksum..." CHECKSUM=$(shasum -a 256 "$DMG_OUTPUT" | awk '{print $1}') ok "SHA256: $CHECKSUM" # ============================================================================= # Summary # ============================================================================= # Mark build as successful before cleanup runs BUILD_SUCCESS=true echo "" ok "=== Build Complete ===" echo " Version: $VERSION" echo " Output: $DMG_OUTPUT" echo " SHA256: $CHECKSUM" if [[ -n "$SIGN_IDENTITY" ]]; then echo " Signed: Yes (identity: $SIGN_IDENTITY)" else echo " Signed: No" fi if $NOTARIZE; then echo " Notarized: Yes" else echo " Notarized: No" fi echo ""