build_mac_universal.sh 6.58 KB
#!/bin/bash
# macOS Build Script - Universal (Intel + Apple Silicon)
# 自动检测架构,自动安装依赖

set -e  # 遇错即停

echo "================================"
echo "Building Gemini Image Generator"
echo "================================"

# 检测架构
ARCH=$(uname -m)
echo "Detected architecture: $ARCH"

# 设置 Homebrew 路径
if [ "$ARCH" = "arm64" ]; then
    BREW_PREFIX="/opt/homebrew"
else
    BREW_PREFIX="/usr/local"
fi

# 检查 Homebrew 是否安装
if ! command -v brew &> /dev/null; then
    echo "Homebrew not found. Installing..."
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    # 添加到当前 shell 的 PATH
    eval "$($BREW_PREFIX/bin/brew shellenv)"
fi

# 确保 brew 在 PATH 中
if ! command -v brew &> /dev/null; then
    eval "$($BREW_PREFIX/bin/brew shellenv)"
fi

# 检查 Python 3.11 是否安装
PYTHON_CMD="$BREW_PREFIX/bin/python3.11"
if [ ! -f "$PYTHON_CMD" ]; then
    echo "Python 3.11 not found. Installing via Homebrew..."
    brew install python@3.11
fi

# 验证 Python 可用
if [ ! -f "$PYTHON_CMD" ]; then
    echo "Error: Failed to install Python 3.11"
    exit 1
fi

echo "Using Python: $PYTHON_CMD"
$PYTHON_CMD --version

# 检查虚拟环境是否有效(macOS 用 bin/activate,Windows 用 Scripts/activate)
if [ ! -f ".venv/bin/activate" ]; then
    echo "Valid virtual environment not found, creating..."
    rm -rf .venv
    "$PYTHON_CMD" -m venv .venv
fi

# 激活虚拟环境
echo "Activating virtual environment..."
source .venv/bin/activate

# 安装依赖
echo "Installing dependencies..."
pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller

# 清理旧构建 (保留 spec 文件,它是构建配置的唯一真相源)
echo "Cleaning previous builds..."
rm -rf build dist

# 构建 (所有配置都在 ZB100ImageGenerator.spec 里)
echo "Building executable..."
pyinstaller ZB100ImageGenerator.spec

# 验证 .app 构建结果
if [ ! -d "dist/ZB100ImageGenerator.app" ]; then
    echo "================================"
    echo "Build failed! (.app not produced)"
    echo "================================"
    exit 1
fi

# 打包 DMG 作为分发格式 (带 drag-to-install 布局)
# 为什么必须: .app 里有嵌套 symlink (PIL/.dylibs -> __dot__dylibs),
# cp/NAS/SMB/微信 等传输方式可能吞掉内层 symlink 导致目标机启动失败.
# .dmg 是 HFS+ 镜像, symlink 原样保存, 用户挂载拖拽即用.
echo "Creating DMG with drag-to-install layout..."
DMG_PATH="dist/ZB100ImageGenerator.dmg"
TMP_DMG="dist/.tmp_rw.dmg"
STAGE="dist/.dmg_stage"
VOL_NAME="ZB100ImageGenerator"

# 1. 准备 staging: .app + Applications 快捷方式 + 背景图
rm -rf "$STAGE"
mkdir -p "$STAGE/.background"
cp -R "dist/ZB100ImageGenerator.app" "$STAGE/"
ln -s /Applications "$STAGE/Applications"

# 用 PIL 生成带箭头+中文提示的背景图 (600x400, 跟 DMG 窗口尺寸匹配)
DMG_BG_PATH="$STAGE/.background/bg.png" python3 <<'PYEOF'
import os
from PIL import Image, ImageDraw, ImageFont

W, H = 600, 400
img = Image.new('RGB', (W, H), color=(248, 248, 250))
d = ImageDraw.Draw(img)

# 中间箭头 (从 .app 指向 Applications)
arrow_y = 200
arrow_color = (120, 120, 125)
d.line([(260, arrow_y), (425, arrow_y)], fill=arrow_color, width=5)
d.polygon(
    [(425, arrow_y - 14), (455, arrow_y), (425, arrow_y + 14)],
    fill=arrow_color,
)

# 底部提示文字
def load_font(size):
    for path in (
        "/System/Library/Fonts/PingFang.ttc",
        "/System/Library/Fonts/STHeiti Medium.ttc",
        "/System/Library/Fonts/Helvetica.ttc",
    ):
        if os.path.exists(path):
            try:
                return ImageFont.truetype(path, size)
            except Exception:
                continue
    return ImageFont.load_default()

font = load_font(22)
text = "将左侧应用拖到右侧 Applications 即完成安装"
bbox = d.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0]
d.text(((W - tw) // 2, 320), text, fill=(90, 90, 95), font=font)

img.save(os.environ['DMG_BG_PATH'])
print(f"[dmg] 背景图已生成: {os.environ['DMG_BG_PATH']}")
PYEOF

# 2. 先卸载可能残留的同名卷, 防止上次失败留下的挂载
hdiutil detach "/Volumes/$VOL_NAME" -force >/dev/null 2>&1 || true

# 3. 创建可写 DMG (UDRW), 便于随后调整窗口布局
rm -f "$TMP_DMG" "$DMG_PATH"
hdiutil create \
    -volname "$VOL_NAME" \
    -srcfolder "$STAGE" \
    -ov -format UDRW \
    "$TMP_DMG"

# 4. 挂载并用 AppleScript 布置窗口 (.app 在左, Applications 在右)
MOUNT_POINT="/Volumes/$VOL_NAME"
hdiutil attach "$TMP_DMG" -readwrite -noverify -noautoopen >/dev/null

# osascript 失败不致命 (可能因 Finder 自动化权限), DMG 仍然可用, 只是没布局
osascript <<OSASCRIPT_EOF || echo "Warning: Finder 布局失败 (可能需要授予 Finder 自动化权限), DMG 仍可用"
tell application "Finder"
    tell disk "$VOL_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
        -- 窗口尺寸匹配背景图 600x400, 坐标系从屏幕左上角算
        set the bounds of container window to {200, 120, 800, 520}
        set theViewOptions to the icon view options of container window
        set arrangement of theViewOptions to not arranged
        set icon size of theViewOptions to 128
        -- 设置背景图 (HFS path, 冒号分隔)
        set background picture of theViewOptions to file ".background:bg.png"
        -- 图标位置要跟背景图里的箭头对齐
        set position of item "ZB100ImageGenerator.app" of container window to {150, 200}
        set position of item "Applications" of container window to {450, 200}
        update without registering applications
        delay 1
        close
    end tell
end tell
OSASCRIPT_EOF

sync
hdiutil detach "$MOUNT_POINT" -force >/dev/null

# 5. 压缩成最终只读 DMG
hdiutil convert "$TMP_DMG" -format UDZO -imagekey zlib-level=9 -o "$DMG_PATH" >/dev/null

# 6. 清理临时
rm -f "$TMP_DMG"
rm -rf "$STAGE"

if [ ! -f "$DMG_PATH" ]; then
    echo "================================"
    echo "Build partially failed: .app OK but DMG creation failed"
    echo "================================"
    exit 1
fi

echo "================================"
echo "Build successful!"
echo "Architecture: $ARCH"
echo "  App:  dist/ZB100ImageGenerator.app  ($(du -sh dist/ZB100ImageGenerator.app | awk '{print $1}'))"
echo "  DMG:  $DMG_PATH  ($(du -sh "$DMG_PATH" | awk '{print $1}'))"
echo "================================"
echo "分发请用 DMG, 不要直接拷贝 .app 目录 (symlink 易丢)."