2026-05-06 19:37:41 -04:00
|
|
|
#!/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
|
|
|
|
|
|
2026-05-06 19:41:07 -04:00
|
|
|
# Platform check: require macOS
|
|
|
|
|
[[ "$(uname)" != "Darwin" ]] && { echo "[ERROR] This script requires macOS" >&2; exit 1; }
|
|
|
|
|
|
2026-05-06 19:37:41 -04:00
|
|
|
# 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 19:41:07 -04:00
|
|
|
# Build success flag for cleanup control
|
|
|
|
|
BUILD_SUCCESS=false
|
|
|
|
|
|
2026-05-06 19:37:41 -04:00
|
|
|
cleanup() {
|
|
|
|
|
info "Cleaning up temporary files..."
|
2026-05-06 19:41:07 -04:00
|
|
|
if [[ "$BUILD_SUCCESS" == false && -d "$TEMP_DIR" ]]; then
|
2026-05-06 19:37:41 -04:00
|
|
|
rm -rf "$TEMP_DIR"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 19:41:07 -04:00
|
|
|
# Trap EXIT for cleanup, ERR for early error exit
|
2026-05-06 19:37:41 -04:00
|
|
|
trap cleanup EXIT
|
2026-05-06 19:41:07 -04:00
|
|
|
trap 'exit 1' ERR
|
2026-05-06 19:37:41 -04:00
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# 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..."
|
2026-05-06 19:41:07 -04:00
|
|
|
mkdir -p "$BUILD_DIR"
|
2026-05-06 19:37:41 -04:00
|
|
|
CMAKE_ARGS=(
|
2026-05-06 19:41:07 -04:00
|
|
|
"-S" "$REPO_ROOT"
|
2026-05-06 19:37:41 -04:00
|
|
|
"-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)
|
2026-05-06 19:41:07 -04:00
|
|
|
# Use absolute path for -qmldir
|
|
|
|
|
macdeployqt "$APP_BUNDLE" -qmldir="$REPO_ROOT/app"
|
2026-05-06 19:37:41 -04:00
|
|
|
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"
|
2026-05-06 19:41:07 -04:00
|
|
|
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
|
2026-05-06 19:37:41 -04:00
|
|
|
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"
|
2026-05-06 19:41:07 -04:00
|
|
|
CREATE_DMG_ARGS+=(--background "$DMG_BG_PATH")
|
2026-05-06 19:37:41 -04:00
|
|
|
ok "SVG converted to PNG"
|
|
|
|
|
else
|
2026-05-06 19:41:07 -04:00
|
|
|
info "rsvg-convert not found — DMG will use plain background (brew install librsvg to add branding)"
|
2026-05-06 19:37:41 -04:00
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2026-05-06 19:41:07 -04:00
|
|
|
# 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
|
|
|
|
|
|
2026-05-06 19:37:41 -04:00
|
|
|
# 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"
|
|
|
|
|
|
2026-05-06 19:41:07 -04:00
|
|
|
# Validate DMG is non-zero
|
|
|
|
|
if [[ ! -s "$DMG_OUTPUT" ]]; then
|
|
|
|
|
error "DMG is empty or missing: $DMG_OUTPUT"
|
2026-05-06 19:37:41 -04:00
|
|
|
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
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
2026-05-06 19:41:07 -04:00
|
|
|
# Mark build as successful before cleanup runs
|
|
|
|
|
BUILD_SUCCESS=true
|
|
|
|
|
|
2026-05-06 19:37:41 -04:00
|
|
|
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 ""
|