dragonmoonlight/scripts/build-installer-mac.sh

362 lines
11 KiB
Bash

#!/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 ""