Some checks are pending
Tests & Quality Checks / Test on Python 3.11 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.10 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-2 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-2 (push) Waiting to run
Tests & Quality Checks / Build Artifacts (push) Blocked by required conditions
Tests & Quality Checks / Build Artifacts-1 (push) Blocked by required conditions
365 lines
9.2 KiB
Bash
365 lines
9.2 KiB
Bash
#!/bin/bash
|
||
# Build macOS DMG package using PyInstaller
|
||
#
|
||
# This script builds the WebDrop Bridge application for macOS and creates
|
||
# a distributable DMG image.
|
||
#
|
||
# Requirements:
|
||
# - PyInstaller 6.0+
|
||
# - Python 3.10+
|
||
# - Xcode Command Line Tools (for code signing)
|
||
# - create-dmg (optional, for custom DMG: brew install create-dmg)
|
||
#
|
||
# Usage:
|
||
# bash build_macos.sh [--sign] [--notarize] [--env-file PATH]
|
||
#
|
||
# Options:
|
||
# --sign Sign app (requires Apple developer certificate)
|
||
# --notarize Notarize app (requires Apple ID)
|
||
# --env-file PATH Use custom .env file (default: project root .env)
|
||
# Build fails if .env doesn't exist
|
||
|
||
set -e # Exit on error
|
||
|
||
# Configuration
|
||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
BUILD_DIR="$PROJECT_ROOT/build"
|
||
DIST_DIR="$BUILD_DIR/dist/macos"
|
||
TEMP_BUILD="$BUILD_DIR/temp/macos"
|
||
SPECS_DIR="$BUILD_DIR/specs"
|
||
SPEC_FILE="$BUILD_DIR/webdrop_bridge.spec"
|
||
BRAND_HELPER="$BUILD_DIR/scripts/brand_config.py"
|
||
|
||
BRAND=""
|
||
APP_NAME="WebDropBridge"
|
||
DMG_VOLUME_NAME="WebDrop Bridge"
|
||
BUNDLE_IDENTIFIER="de.him_tools.webdrop-bridge"
|
||
VERSION=""
|
||
|
||
# Default .env file
|
||
ENV_FILE="$PROJECT_ROOT/.env"
|
||
|
||
# Parse arguments
|
||
SIGN_APP=0
|
||
NOTARIZE_APP=0
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--sign)
|
||
SIGN_APP=1
|
||
shift
|
||
;;
|
||
--notarize)
|
||
NOTARIZE_APP=1
|
||
shift
|
||
;;
|
||
--env-file)
|
||
ENV_FILE="$2"
|
||
shift 2
|
||
;;
|
||
--brand)
|
||
BRAND="$2"
|
||
shift 2
|
||
;;
|
||
*)
|
||
echo "Unknown option: $1"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Validate env file
|
||
if [ ! -f "$ENV_FILE" ]; then
|
||
echo "❌ Configuration file not found: $ENV_FILE"
|
||
echo "Please provide a valid .env file or use --env-file parameter"
|
||
exit 1
|
||
fi
|
||
|
||
echo "📋 Using configuration: $ENV_FILE"
|
||
|
||
if [ -z "$BRAND" ]; then
|
||
BRAND="webdrop_bridge"
|
||
fi
|
||
|
||
eval "$(python3 "$BRAND_HELPER" env --brand "$BRAND")"
|
||
APP_NAME="$WEBDROP_ASSET_PREFIX"
|
||
DMG_VOLUME_NAME="$WEBDROP_APP_DISPLAY_NAME"
|
||
BUNDLE_IDENTIFIER="$WEBDROP_BUNDLE_ID"
|
||
DIST_DIR="$BUILD_DIR/dist/macos/$WEBDROP_BRAND_ID"
|
||
TEMP_BUILD="$BUILD_DIR/temp/macos/$WEBDROP_BRAND_ID"
|
||
|
||
if [ -n "$WEBDROP_APP_DISPLAY_NAME" ]; then
|
||
echo "🏷️ Building brand: $WEBDROP_APP_DISPLAY_NAME ($WEBDROP_BRAND_ID)"
|
||
fi
|
||
|
||
VERSION="$(python3 -c "from pathlib import Path; import sys; sys.path.insert(0, str(Path(r'$BUILD_DIR/scripts').resolve())); from version_utils import get_current_version; print(get_current_version())")"
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Logging functions
|
||
log_info() {
|
||
echo -e "${BLUE}ℹ️${NC} $1"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e "${GREEN}✅${NC} $1"
|
||
}
|
||
|
||
log_warning() {
|
||
echo -e "${YELLOW}⚠️${NC} $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}❌${NC} $1"
|
||
}
|
||
|
||
# Main build function
|
||
main() {
|
||
echo "=========================================="
|
||
echo "🚀 WebDrop Bridge macOS Build"
|
||
echo "=========================================="
|
||
echo ""
|
||
|
||
# Check prerequisites
|
||
check_prerequisites
|
||
|
||
# Clean previous builds
|
||
clean_builds
|
||
|
||
# Build with PyInstaller
|
||
build_executable
|
||
|
||
# Create DMG
|
||
create_dmg
|
||
|
||
# Optional: Sign and notarize
|
||
if [ $SIGN_APP -eq 1 ]; then
|
||
sign_app
|
||
fi
|
||
|
||
if [ $NOTARIZE_APP -eq 1 ]; then
|
||
notarize_app
|
||
fi
|
||
|
||
echo ""
|
||
echo "=========================================="
|
||
log_success "Build completed successfully"
|
||
echo "=========================================="
|
||
echo ""
|
||
log_info "Output: $DIST_DIR/"
|
||
ls -lh "$DIST_DIR"
|
||
}
|
||
|
||
check_prerequisites() {
|
||
log_info "Checking prerequisites..."
|
||
|
||
# Check Python
|
||
if ! command -v python3 &> /dev/null; then
|
||
log_error "Python 3 not found"
|
||
exit 1
|
||
fi
|
||
log_success "Python 3 found: $(python3 --version)"
|
||
|
||
# Check PyInstaller
|
||
if ! python3 -m pip show pyinstaller &> /dev/null; then
|
||
log_error "PyInstaller not installed. Run: pip install pyinstaller"
|
||
exit 1
|
||
fi
|
||
log_success "PyInstaller installed"
|
||
|
||
# Check spec file
|
||
if [ ! -f "$SPEC_FILE" ]; then
|
||
log_error "Spec file not found: $SPEC_FILE"
|
||
exit 1
|
||
fi
|
||
log_success "Spec file found"
|
||
|
||
echo ""
|
||
}
|
||
|
||
clean_builds() {
|
||
log_info "Cleaning previous builds..."
|
||
|
||
for dir in "$DIST_DIR" "$TEMP_BUILD" "$SPECS_DIR"; do
|
||
if [ -d "$dir" ]; then
|
||
rm -rf "$dir"
|
||
log_success "Removed $dir"
|
||
fi
|
||
done
|
||
|
||
mkdir -p "$DIST_DIR" "$TEMP_BUILD" "$SPECS_DIR"
|
||
echo ""
|
||
}
|
||
|
||
build_executable() {
|
||
log_info "Building macOS executable with PyInstaller..."
|
||
echo ""
|
||
|
||
# Create bundled runtime .env with brand defaults so first launch
|
||
# uses brand-specific app name and config directory.
|
||
BUNDLED_ENV_FILE="$TEMP_BUILD/.env"
|
||
cp "$ENV_FILE" "$BUNDLED_ENV_FILE"
|
||
{
|
||
echo ""
|
||
echo "# Brand-specific defaults added during packaging"
|
||
echo "APP_NAME=\"$WEBDROP_APP_DISPLAY_NAME\""
|
||
echo "BRAND_ID=\"$WEBDROP_BRAND_ID\""
|
||
echo "APP_CONFIG_DIR_NAME=\"$WEBDROP_CONFIG_DIR_NAME\""
|
||
echo "UPDATE_CHANNEL=\"$WEBDROP_UPDATE_CHANNEL\""
|
||
echo "TOOLBAR_ICON_HOME=\"$WEBDROP_TOOLBAR_ICON_HOME\""
|
||
echo "TOOLBAR_ICON_RELOAD=\"$WEBDROP_TOOLBAR_ICON_RELOAD\""
|
||
echo "TOOLBAR_ICON_OPEN=\"$WEBDROP_TOOLBAR_ICON_OPEN\""
|
||
echo "TOOLBAR_ICON_OPENWITH=\"$WEBDROP_TOOLBAR_ICON_OPENWITH\""
|
||
} >> "$BUNDLED_ENV_FILE"
|
||
|
||
# Export env file for spec file to pick up
|
||
export WEBDROP_ENV_FILE="$BUNDLED_ENV_FILE"
|
||
export WEBDROP_VERSION="$VERSION"
|
||
export WEBDROP_BUNDLE_ID="$BUNDLE_IDENTIFIER"
|
||
|
||
python3 -m PyInstaller \
|
||
--distpath="$DIST_DIR" \
|
||
--buildpath="$TEMP_BUILD" \
|
||
--specpath="$SPECS_DIR" \
|
||
"$SPEC_FILE"
|
||
|
||
if [ ! -d "$DIST_DIR/$APP_NAME.app" ]; then
|
||
log_error "Application bundle not created"
|
||
exit 1
|
||
fi
|
||
|
||
log_success "Application bundle built successfully"
|
||
log_info "Output: $DIST_DIR/$APP_NAME.app"
|
||
echo ""
|
||
}
|
||
|
||
create_dmg() {
|
||
log_info "Creating DMG package..."
|
||
echo ""
|
||
|
||
DMG_FILE="$DIST_DIR/${APP_NAME}-${VERSION}-macos-universal.dmg"
|
||
|
||
# Remove existing DMG
|
||
if [ -f "$DMG_FILE" ]; then
|
||
rm -f "$DMG_FILE"
|
||
fi
|
||
|
||
# Check if create-dmg is available
|
||
if command -v create-dmg &> /dev/null; then
|
||
log_info "Using create-dmg for professional DMG..."
|
||
|
||
create-dmg \
|
||
--volname "$DMG_VOLUME_NAME" \
|
||
--icon-size 128 \
|
||
--window-size 512 400 \
|
||
--app-drop-link 380 200 \
|
||
"$DMG_FILE" \
|
||
"$DIST_DIR/$APP_NAME.app"
|
||
else
|
||
log_warning "create-dmg not found, using hdiutil (less stylish)"
|
||
log_info "For professional DMG: brew install create-dmg"
|
||
|
||
# Create temporary DMG directory structure
|
||
DMG_TEMP="$TEMP_BUILD/dmg_contents"
|
||
mkdir -p "$DMG_TEMP"
|
||
|
||
# Copy app bundle
|
||
cp -r "$DIST_DIR/$APP_NAME.app" "$DMG_TEMP/"
|
||
|
||
# Create symlink to Applications folder
|
||
ln -s /Applications "$DMG_TEMP/Applications"
|
||
|
||
# Create DMG
|
||
hdiutil create \
|
||
-volname "$DMG_VOLUME_NAME" \
|
||
-srcfolder "$DMG_TEMP" \
|
||
-ov \
|
||
-format UDZO \
|
||
"$DMG_FILE"
|
||
|
||
# Clean up
|
||
rm -rf "$DMG_TEMP"
|
||
fi
|
||
|
||
if [ ! -f "$DMG_FILE" ]; then
|
||
log_error "DMG file not created"
|
||
exit 1
|
||
fi
|
||
|
||
# Get file size
|
||
SIZE=$(du -h "$DMG_FILE" | cut -f1)
|
||
log_success "DMG created successfully"
|
||
log_info "Output: $DMG_FILE (Size: $SIZE)"
|
||
shasum -a 256 "$DMG_FILE" | awk '{print $1}' > "$DMG_FILE.sha256"
|
||
log_info "Checksum: $DMG_FILE.sha256"
|
||
echo ""
|
||
}
|
||
|
||
sign_app() {
|
||
log_info "Signing application..."
|
||
echo ""
|
||
|
||
# Get signing identity from environment or use ad-hoc
|
||
SIGNING_ID="${APPLE_SIGNING_ID:--}"
|
||
|
||
codesign \
|
||
--deep \
|
||
--force \
|
||
--verify \
|
||
--verbose \
|
||
--options=runtime \
|
||
--sign "$SIGNING_ID" \
|
||
"$DIST_DIR/$APP_NAME.app"
|
||
|
||
if [ $? -ne 0 ]; then
|
||
log_error "Code signing failed"
|
||
exit 1
|
||
fi
|
||
|
||
log_success "Application signed successfully"
|
||
echo ""
|
||
}
|
||
|
||
notarize_app() {
|
||
log_info "Notarizing application..."
|
||
echo ""
|
||
|
||
# Requires:
|
||
# - APPLE_ID environment variable
|
||
# - APPLE_PASSWORD environment variable (app-specific password)
|
||
# - APPLE_TEAM_ID environment variable
|
||
|
||
if [ -z "$APPLE_ID" ] || [ -z "$APPLE_PASSWORD" ]; then
|
||
log_error "APPLE_ID and APPLE_PASSWORD environment variables required for notarization"
|
||
return 1
|
||
fi
|
||
|
||
DMG_FILE="$DIST_DIR/${APP_NAME}-${VERSION}.dmg"
|
||
|
||
# Upload for notarization
|
||
log_info "Uploading to Apple Notarization Service..."
|
||
xcrun notarytool submit "$DMG_FILE" \
|
||
--apple-id "$APPLE_ID" \
|
||
--password "$APPLE_PASSWORD" \
|
||
--team-id "${APPLE_TEAM_ID}" \
|
||
--wait
|
||
|
||
if [ $? -ne 0 ]; then
|
||
log_error "Notarization failed"
|
||
return 1
|
||
fi
|
||
|
||
# Staple the notarization
|
||
xcrun stapler staple "$DMG_FILE"
|
||
|
||
log_success "Application notarized successfully"
|
||
echo ""
|
||
}
|
||
|
||
# Run main function
|
||
main
|