From f6e02a5469599693a5cf8996f88654344672ef25 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Wed, 6 May 2026 19:37:41 -0400 Subject: [PATCH] Add build-installer-mac.sh: end-to-end macOS DMG build pipeline --- scripts/build-installer-mac.sh | 345 +++++++++++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 scripts/build-installer-mac.sh diff --git a/scripts/build-installer-mac.sh b/scripts/build-installer-mac.sh new file mode 100644 index 0000000..b24f1f4 --- /dev/null +++ b/scripts/build-installer-mac.sh @@ -0,0 +1,345 @@ +#!/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 + +# 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 +} + +cleanup() { + info "Cleaning up temporary files..." + if [[ -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + fi +} + +# Trap EXIT to ensure cleanup +trap cleanup EXIT + +# ============================================================================= +# 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..." +CMAKE_ARGS=( + "-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) +macdeployqt "$APP_BUNDLE" -qmldir=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" +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" + ok "SVG converted to PNG" + else + info "rsvg-convert not found; creating placeholder background" + # Create a simple dark placeholder PNG (if needed; create-dmg works without a background) + fi +fi + +# Use create-dmg +DMG_OUTPUT="$OUTPUT_DIR/DragonMoonlight-$VERSION.dmg" +rm -f "$DMG_OUTPUT" + +CREATE_DMG_ARGS=( + --volname "DragonMoonlight" + --volicon "$APP_BUNDLE/Contents/Resources/icon.icns" + --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 +) + +if [[ -f "$DMG_BG_PATH" ]]; then + CREATE_DMG_ARGS+=(--background "$DMG_BG_PATH") +fi + +create-dmg "${CREATE_DMG_ARGS[@]}" "$DMG_OUTPUT" "$(dirname "$APP_BUNDLE")/DragonMoonlight.app" + +if [[ ! -f "$DMG_OUTPUT" ]]; then + error "Failed to create DMG at $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 +# ============================================================================= + +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 ""