#!/bin/zsh set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" APP_DIR="$ROOT_DIR/dist/boss-agent.app" CONTENTS_DIR="$APP_DIR/Contents" MACOS_DIR="$CONTENTS_DIR/MacOS" RESOURCES_DIR="$CONTENTS_DIR/Resources" SOURCE_FILE="$ROOT_DIR/apps/boss-agent-mac/Sources/BossAgentApp.swift" BINARY_PATH="$MACOS_DIR/boss-agent" ICONSET_DIR="$RESOURCES_DIR/BossAgent.iconset" ICON_PATH="$RESOURCES_DIR/BossAgent.icns" SIGNING_IDENTITY="${BOSS_AGENT_CODESIGN_IDENTITY:-}" NOTARIZE="${BOSS_AGENT_NOTARIZE:-0}" NOTARY_PROFILE="${BOSS_AGENT_NOTARY_PROFILE:-}" NOTARY_APPLE_ID="${BOSS_AGENT_NOTARY_APPLE_ID:-}" NOTARY_TEAM_ID="${BOSS_AGENT_NOTARY_TEAM_ID:-}" NOTARY_PASSWORD="${BOSS_AGENT_NOTARY_PASSWORD:-}" if ! command -v swiftc >/dev/null 2>&1; then echo "swiftc not found. Install Xcode Command Line Tools first." >&2 exit 1 fi if ! command -v iconutil >/dev/null 2>&1; then echo "iconutil not found. Install Xcode Command Line Tools first." >&2 exit 1 fi if [[ -z "$SIGNING_IDENTITY" ]] && command -v security >/dev/null 2>&1; then if [[ "$NOTARIZE" == "1" ]]; then SIGNING_IDENTITY="$( security find-identity -v -p codesigning 2>/dev/null \ | awk -F'"' '/"Developer ID Application:/ { print $2; exit }' )" else SIGNING_IDENTITY="$( security find-identity -v -p codesigning 2>/dev/null \ | awk -F'"' '/"Apple Development:|Developer ID Application:|Mac Developer:|Boss Agent/ { print $2; exit }' )" fi fi if [[ -z "$SIGNING_IDENTITY" ]]; then if [[ "$NOTARIZE" == "1" ]]; then echo "boss-agent: BOSS_AGENT_NOTARIZE=1 requires a Developer ID Application signing identity." >&2 exit 1 fi SIGNING_IDENTITY="-" echo "boss-agent: no stable code signing identity found; falling back to ad-hoc signing." >&2 else echo "boss-agent: signing with identity: $SIGNING_IDENTITY" >&2 fi rm -rf "$APP_DIR" mkdir -p "$MACOS_DIR" "$RESOURCES_DIR" swiftc "$SOURCE_FILE" \ -o "$BINARY_PATH" \ -framework Cocoa \ -framework WebKit \ -framework ApplicationServices chmod +x "$BINARY_PATH" python3 - "$ICONSET_DIR" <<'PY' import os import struct import sys import zlib iconset_dir = sys.argv[1] os.makedirs(iconset_dir, exist_ok=True) targets = { "icon_16x16.png": 16, "icon_16x16@2x.png": 32, "icon_32x32.png": 32, "icon_32x32@2x.png": 64, "icon_128x128.png": 128, "icon_128x128@2x.png": 256, "icon_256x256.png": 256, "icon_256x256@2x.png": 512, "icon_512x512.png": 512, "icon_512x512@2x.png": 1024, } def rounded_rect(px, py, x0, y0, x1, y1, radius): cx = min(max(px, x0 + radius), x1 - radius) cy = min(max(py, y0 + radius), y1 - radius) return (px - cx) * (px - cx) + (py - cy) * (py - cy) <= radius * radius def rgba_at(px, py): if rounded_rect(px, py, 82, 82, 942, 942, 210): white = ( rounded_rect(px, py, 278, 245, 406, 779, 64) or rounded_rect(px, py, 350, 245, 745, 518, 132) or rounded_rect(px, py, 350, 506, 745, 779, 132) ) cutout = ( rounded_rect(px, py, 462, 332, 619, 438, 54) or rounded_rect(px, py, 462, 592, 635, 698, 54) ) if white and not cutout: return (255, 255, 255, 255) return (7, 193, 96, 255) if rounded_rect(px, py - 18, 82, 82, 942, 942, 210): return (7, 24, 16, 28) return (0, 0, 0, 0) def write_png(path, size): rows = [] for y in range(size): row = bytearray([0]) for x in range(size): px = (x + 0.5) * 1024 / size py = (y + 0.5) * 1024 / size row.extend(rgba_at(px, py)) rows.append(bytes(row)) raw = b"".join(rows) def chunk(kind, data): return ( struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF) ) with open(path, "wb") as fp: fp.write(b"\x89PNG\r\n\x1a\n") fp.write(chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0))) fp.write(chunk(b"IDAT", zlib.compress(raw, 9))) fp.write(chunk(b"IEND", b"")) for name, size in targets.items(): write_png(os.path.join(iconset_dir, name), size) PY iconutil -c icns "$ICONSET_DIR" -o "$ICON_PATH" rm -rf "$ICONSET_DIR" cat > "$CONTENTS_DIR/Info.plist" <<'PLIST' CFBundleDevelopmentRegion zh_CN CFBundleExecutable boss-agent CFBundleIdentifier com.hyzq.boss.agent CFBundleInfoDictionaryVersion 6.0 CFBundleName boss-agent CFBundleDisplayName boss-agent CFBundleIconFile BossAgent.icns CFBundleURLTypes CFBundleURLName com.hyzq.boss.agent CFBundleURLSchemes boss-agent CFBundlePackageType APPL CFBundleShortVersionString 0.1.0 CFBundleVersion 1 LSMinimumSystemVersion 13.0 NSHighResolutionCapable NSScreenCaptureUsageDescription boss-agent 需要读取屏幕画面,用于识别桌面状态、系统弹窗和远程控制结果。 PLIST plutil -lint "$CONTENTS_DIR/Info.plist" >/dev/null if [[ "$NOTARIZE" == "1" ]]; then if ! command -v xcrun >/dev/null 2>&1; then echo "boss-agent: xcrun is required for notarization." >&2 exit 1 fi codesign --force --deep --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$APP_DIR" >/dev/null NOTARY_ZIP="$ROOT_DIR/dist/boss-agent-notary.zip" rm -f "$NOTARY_ZIP" ( cd "$ROOT_DIR/dist" ditto -c -k --keepParent "boss-agent.app" "$NOTARY_ZIP" ) NOTARY_ARGS=() if [[ -n "$NOTARY_PROFILE" ]]; then NOTARY_ARGS=(--keychain-profile "$NOTARY_PROFILE") elif [[ -n "$NOTARY_APPLE_ID" && -n "$NOTARY_TEAM_ID" && -n "$NOTARY_PASSWORD" ]]; then NOTARY_ARGS=(--apple-id "$NOTARY_APPLE_ID" --team-id "$NOTARY_TEAM_ID" --password "$NOTARY_PASSWORD") else echo "boss-agent: notarization requires BOSS_AGENT_NOTARY_PROFILE or Apple ID/team/password env vars." >&2 exit 1 fi xcrun notarytool submit "$NOTARY_ZIP" "${NOTARY_ARGS[@]}" --wait >/dev/null xcrun stapler staple "$APP_DIR" >/dev/null rm -f "$NOTARY_ZIP" else codesign --force --deep --timestamp=none --sign "$SIGNING_IDENTITY" "$APP_DIR" >/dev/null fi echo "$APP_DIR"