jmanus/script/create_dmg_embedded.sh
cuijiawang 0982edf55f init
2025-08-01 10:24:29 +08:00

473 lines
14 KiB
Bash

#!/bin/bash
/*
* Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# DMG creation script for JManus with embedded JDK on macOS
# This script creates a distributable DMG package with embedded JDK
set -e
# Cleanup function for script interruption
cleanup_on_exit() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
echo "Script interrupted, cleaning up..."
# More comprehensive cleanup for Intel Macs
echo "Performing comprehensive cleanup..."
# Get all disk image info
local dmg_info=$(hdiutil info 2>/dev/null || true)
# Unmount any JManus related mounts
echo "$dmg_info" | grep -E "(JManus|temp.*JManus)" | awk '{print $1}' | while read -r device; do
if [ -n "$device" ]; then
echo "Force unmounting $device"
hdiutil detach "$device" -force 2>/dev/null || true
fi
done
# Clean up project directory DMG files
echo "$dmg_info" | grep -E "$PROJECT_ROOT" | awk '{print $1}' | while read -r device; do
if [ -n "$device" ]; then
echo "Force unmounting project DMG: $device"
hdiutil detach "$device" -force 2>/dev/null || true
fi
done
# Remove temporary files
rm -f "$PROJECT_ROOT/dist/temp-"*".dmg" 2>/dev/null || true
rm -f "$PROJECT_ROOT/dist/"*".dmg.sparseimage" 2>/dev/null || true
rm -f "$PROJECT_ROOT/dist/"*".tmp" 2>/dev/null || true
# Force sync
sync
sleep 2
fi
}
# Set up cleanup on script exit
trap cleanup_on_exit EXIT
# Configuration
APP_NAME="JManus"
APP_BUNDLE_NAME="JManus.app"
DMG_NAME="JManus-Installer"
TEMP_DMG_NAME="temp-${DMG_NAME}"
VOLUME_NAME="JManus Installer"
# Get script directory and project root early
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
# Parse arguments
PLATFORM=""
VERSION="3.0.0-SNAPSHOT"
while [[ $# -gt 0 ]]; do
case $1 in
--platform)
PLATFORM="$2"
shift 2
;;
--version)
VERSION="$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
if [ -z "$PLATFORM" ]; then
echo "Usage: $0 --platform <platform> [--version <version>]"
echo "Example: $0 --platform macos-aarch64 --version 3.0.0"
exit 1
fi
echo "Creating DMG for platform: $PLATFORM"
echo "Version: $VERSION"
# Set distribution directory
DIST_DIR="$PROJECT_ROOT/dist/$PLATFORM"
# Check if distribution exists
if [ ! -d "$DIST_DIR" ]; then
echo "Error: Distribution directory not found at $DIST_DIR"
echo "Please run build_embedded_jdk.sh first"
exit 1
fi
# Create temporary directory structure
TEMP_DIR=$(mktemp -d)
APP_BUNDLE_DIR="$TEMP_DIR/$APP_BUNDLE_NAME"
APP_CONTENTS_DIR="$APP_BUNDLE_DIR/Contents"
APP_MACOS_DIR="$APP_CONTENTS_DIR/MacOS"
APP_RESOURCES_DIR="$APP_CONTENTS_DIR/Resources"
mkdir -p "$APP_MACOS_DIR"
mkdir -p "$APP_RESOURCES_DIR"
# Copy the entire distribution to the app bundle
cp -r "$DIST_DIR"/* "$APP_RESOURCES_DIR/"
# Create main executable wrapper script
cat > "$APP_MACOS_DIR/JManus" << 'EOF'
#!/bin/bash
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_RESOURCES="$SCRIPT_DIR/../Resources"
# Change to resources directory
cd "$APP_RESOURCES"
# Export API key if set
if [ -n "$DASHSCOPE_API_KEY" ]; then
export DASHSCOPE_API_KEY
fi
# Launch the application using the embedded startup script
exec "$APP_RESOURCES/bin/start_jmanus.sh" --terminal
EOF
chmod +x "$APP_MACOS_DIR/JManus"
# Create Info.plist for the app bundle
cat > "$APP_CONTENTS_DIR/Info.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>JManus</string>
<key>CFBundleIdentifier</key>
<string>com.alibaba.spring-ai.jmanus</string>
<key>CFBundleName</key>
<string>JManus</string>
<key>CFBundleDisplayName</key>
<string>JManus</string>
<key>CFBundleVersion</key>
<string>$VERSION</string>
<key>CFBundleShortVersionString</key>
<string>$VERSION</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSRequiresAquaSystemAppearance</key>
<false/>
<key>LSEnvironment</key>
<dict>
<key>LC_CTYPE</key>
<string>UTF-8</string>
</dict>
</dict>
</plist>
EOF
# Create PkgInfo
echo "APPLNONE" > "$APP_CONTENTS_DIR/PkgInfo"
# Create the DMG staging area
DMG_STAGING_DIR="$TEMP_DIR/dmg"
mkdir -p "$DMG_STAGING_DIR"
# Copy app bundle to staging area
cp -r "$APP_BUNDLE_DIR" "$DMG_STAGING_DIR/"
# Create a symlink to Applications folder
ln -s /Applications "$DMG_STAGING_DIR/Applications"
# Add additional files
mkdir -p "$DMG_STAGING_DIR/Documentation"
cp "$DIST_DIR/docs"/*.md "$DMG_STAGING_DIR/Documentation/" 2>/dev/null || true
cp "$DIST_DIR/BUILD_INFO.txt" "$DMG_STAGING_DIR/" 2>/dev/null || true
# Calculate DMG size (add 50MB buffer)
SIZE_MB=$(du -sm "$DMG_STAGING_DIR" | cut -f1)
SIZE_MB=$((SIZE_MB + 50))
# Create the DMG
DMG_PATH="$PROJECT_ROOT/dist/${DMG_NAME}-${VERSION}-${PLATFORM}.dmg"
TEMP_DMG_PATH="$PROJECT_ROOT/dist/${TEMP_DMG_NAME}.dmg"
echo "Creating DMG (${SIZE_MB}MB)..."
# Remove existing DMG files
rm -f "$DMG_PATH" "$TEMP_DMG_PATH"
# Function to unmount any existing DMG mounts
unmount_existing_dmgs() {
echo "Checking for existing DMG mounts..."
# First, get all disk images info
local dmg_info=$(hdiutil info 2>/dev/null || true)
# Check for any JManus related mounts by volume name
echo "$dmg_info" | grep -E "/Volumes/.*JManus" | while read -r line; do
local device=$(echo "$line" | awk '{print $1}')
if [ -n "$device" ]; then
echo "Unmounting JManus volume: $device"
hdiutil detach "$device" -force 2>/dev/null || true
fi
done
# Check for any temp DMG files that might be mounted
echo "$dmg_info" | grep -E "temp.*JManus.*Installer" | while read -r line; do
local device=$(echo "$line" | awk '{print $1}')
if [ -n "$device" ]; then
echo "Unmounting temp DMG: $device"
hdiutil detach "$device" -force 2>/dev/null || true
fi
done
# Check for any DMG files from our project directory that might be mounted
echo "$dmg_info" | grep -E "$PROJECT_ROOT" | while read -r line; do
local device=$(echo "$line" | awk '{print $1}')
if [ -n "$device" ]; then
echo "Unmounting project DMG: $device"
hdiutil detach "$device" -force 2>/dev/null || true
fi
done
# Additional cleanup - unmount any disk images that might be related
local mounted_volumes=$(mount | grep -E "JManus|temp.*dmg" | awk '{print $1}' || true)
if [ -n "$mounted_volumes" ]; then
echo "Found additional mounted volumes to clean up..."
echo "$mounted_volumes" | while read -r device; do
if [ -n "$device" ]; then
echo "Unmounting volume: $device"
hdiutil detach "$device" -force 2>/dev/null || true
fi
done
fi
# Wait longer for system to clean up on older macOS versions
echo "Waiting for system cleanup..."
sleep 5
# Force sync to ensure all disk operations are complete
sync
}
# Unmount any existing DMG mounts
unmount_existing_dmgs
# Additional cleanup for Intel Macs - remove any leftover files
echo "Cleaning up any leftover DMG files..."
rm -f "$PROJECT_ROOT/dist/"*".dmg.sparseimage" 2>/dev/null || true
rm -f "$PROJECT_ROOT/dist/"*".tmp" 2>/dev/null || true
# Create temporary DMG with additional error handling
echo "Creating temporary DMG..."
# Try to create the DMG with retry logic
for create_attempt in 1 2 3; do
echo "DMG creation attempt $create_attempt..."
if [ $create_attempt -gt 1 ]; then
echo "Retrying DMG creation after cleanup..."
unmount_existing_dmgs
rm -f "$TEMP_DMG_PATH" 2>/dev/null || true
sleep 3
fi
if hdiutil create -srcfolder "$DMG_STAGING_DIR" \
-volname "$VOLUME_NAME" \
-fs HFS+ \
-format UDRW \
-size "${SIZE_MB}m" \
"$TEMP_DMG_PATH" 2>/dev/null; then
echo "DMG creation successful on attempt $create_attempt"
break
else
echo "DMG creation failed on attempt $create_attempt"
if [ $create_attempt -eq 3 ]; then
echo "Failed to create DMG after 3 attempts"
exit 1
fi
echo "Waiting before retry..."
sleep 5
fi
done
# Verify DMG was created successfully
if [ ! -f "$TEMP_DMG_PATH" ]; then
echo "Error: Failed to create temporary DMG"
exit 1
fi
echo "Temporary DMG created successfully: $TEMP_DMG_PATH"
# Mount the temporary DMG
echo "Mounting temporary DMG for configuration..."
MOUNT_OUTPUT=$(hdiutil attach -readwrite -noverify -noautoopen "$TEMP_DMG_PATH" 2>&1)
MOUNT_RESULT=$?
if [ $MOUNT_RESULT -ne 0 ]; then
echo "Error: Failed to mount temporary DMG"
echo "Mount output: $MOUNT_OUTPUT"
rm -f "$TEMP_DMG_PATH"
exit 1
fi
MOUNT_POINT=$(echo "$MOUNT_OUTPUT" | grep -E '/Volumes/' | tail -1)
MOUNT_DEVICE=$(echo "$MOUNT_OUTPUT" | grep -E '^/dev/disk[0-9]+s[0-9]+' | awk '{print $1}')
if [ -z "$MOUNT_POINT" ] || [ -z "$MOUNT_DEVICE" ]; then
echo "Error: Could not determine mount point or device"
echo "Mount output: $MOUNT_OUTPUT"
rm -f "$TEMP_DMG_PATH"
exit 1
fi
echo "DMG mounted at: $MOUNT_POINT (device: $MOUNT_DEVICE)"
# Set DMG window properties and background
if [ -n "$MOUNT_POINT" ]; then
echo "Configuring DMG appearance..."
# Create .DS_Store for window settings
echo '
on run argv
tell application "Finder"
tell disk "'$VOLUME_NAME'"
open
set current view of container window to icon view
set toolbar visible of container window to false
set statusbar visible of container window to false
set the bounds of container window to {100, 100, 600, 400}
set viewOptions to the icon view options of container window
set arrangement of viewOptions to not arranged
set icon size of viewOptions to 72
set position of item "JManus.app" of container window to {150, 120}
set position of item "Applications" of container window to {350, 120}
if exists item "Documentation" then
set position of item "Documentation" of container window to {400, 220}
end if
update without registering applications
delay 2
end tell
end tell
end run
' | osascript
# Give the system time to write the .DS_Store file
sleep 3
# Safely unmount with retry
echo "Unmounting temporary DMG..."
for i in 1 2 3; do
if hdiutil detach "$MOUNT_DEVICE" 2>/dev/null; then
echo "Successfully unmounted on attempt $i"
break
else
echo "Unmount attempt $i failed, retrying..."
sleep 2
if [ $i -eq 3 ]; then
echo "Force unmounting..."
hdiutil detach "$MOUNT_DEVICE" -force || true
fi
fi
done
else
echo "Warning: Mount point not available, skipping DMG appearance configuration"
fi
# Convert to compressed read-only DMG
echo "Compressing DMG..."
# Function to cleanup on conversion failure
cleanup_on_failure() {
echo "Cleaning up after conversion failure..."
# Unmount any mounts that might still be active
unmount_existing_dmgs
# Remove temp DMG file
rm -f "$TEMP_DMG_PATH"
}
# Add retry logic for hdiutil convert with proper cleanup
for attempt in 1 2 3; do
echo "DMG compression attempt $attempt..."
# Ensure clean state before each attempt
if [ $attempt -gt 1 ]; then
cleanup_on_failure
sleep 5
fi
if hdiutil convert "$TEMP_DMG_PATH" \
-format UDZO \
-imagekey zlib-level=9 \
-o "$DMG_PATH" 2>/dev/null; then
echo "DMG compression successful on attempt $attempt"
break
else
echo "DMG compression failed on attempt $attempt"
if [ $attempt -eq 3 ]; then
echo "Failed to compress DMG after 3 attempts"
cleanup_on_failure
exit 1
fi
echo "Waiting 5 seconds before retry..."
sleep 5
fi
done
# Clean up
rm -f "$TEMP_DMG_PATH"
rm -rf "$TEMP_DIR"
# Verify the DMG
if [ -f "$DMG_PATH" ]; then
DMG_SIZE=$(ls -lh "$DMG_PATH" | awk '{print $5}')
echo "DMG created successfully: $DMG_PATH ($DMG_SIZE)"
# Test mount the DMG to verify it works
echo "Verifying DMG..."
TEST_MOUNT_OUTPUT=$(hdiutil attach -readonly -noverify -noautoopen "$DMG_PATH" 2>&1)
TEST_MOUNT_RESULT=$?
if [ $TEST_MOUNT_RESULT -eq 0 ]; then
TEST_MOUNT_DEVICE=$(echo "$TEST_MOUNT_OUTPUT" | grep -E '^/dev/disk[0-9]+s[0-9]+' | awk '{print $1}')
if [ -n "$TEST_MOUNT_DEVICE" ]; then
echo "DMG verification successful"
hdiutil detach "$TEST_MOUNT_DEVICE" >/dev/null 2>&1
else
echo "Warning: Could not determine test mount device"
fi
else
echo "Warning: DMG verification failed"
echo "Verification output: $TEST_MOUNT_OUTPUT"
fi
else
echo "Error: Failed to create DMG"
exit 1
fi
echo "DMG creation completed: $DMG_PATH"