build-macos.sh (6590B)
1 #!/usr/bin/env bash 2 # Build Navi.app for macOS and package it as Navi-<version>-<arch>.zip. 3 # 4 # Usage: 5 # scripts/build-macos.sh # release build, host arch 6 # ARCH=universal scripts/build-macos.sh # arm64 + x86_64 universal binary 7 # 8 # Output: dist/Navi.app and dist/Navi-<version>-<arch>.zip 9 # 10 # Requirements: Rust toolchain (rustup), Xcode CLT for codesign / ditto. 11 # For ARCH=universal: rustup target add x86_64-apple-darwin aarch64-apple-darwin 12 set -euo pipefail 13 14 ROOT="$(cd "$(dirname "$0")/.." && pwd)" 15 cd "$ROOT" 16 17 ARCH="${ARCH:-host}" 18 VERSION="$(grep '^version' navi/Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')" 19 APP="dist/Navi.app" 20 ZIP="dist/Navi-${VERSION}-${ARCH}.zip" 21 22 echo "==> Navi $VERSION arch=$ARCH" 23 24 # ── Build ──────────────────────────────────────────────────────────────────── 25 case "$ARCH" in 26 host) 27 cargo build --release -p navi 28 BIN="target/release/navi" 29 ;; 30 universal) 31 rustup target add x86_64-apple-darwin aarch64-apple-darwin >/dev/null 2>&1 || true 32 cargo build --release -p navi --target x86_64-apple-darwin 33 cargo build --release -p navi --target aarch64-apple-darwin 34 mkdir -p target/universal/release 35 BIN="target/universal/release/navi" 36 lipo -create -output "$BIN" \ 37 target/x86_64-apple-darwin/release/navi \ 38 target/aarch64-apple-darwin/release/navi 39 ;; 40 *) 41 echo "unknown ARCH=$ARCH (expected host|universal)" >&2 42 exit 2 43 ;; 44 esac 45 46 # ── Bundle ─────────────────────────────────────────────────────────────────── 47 echo "==> Assembling $APP" 48 rm -rf "$APP" "$ZIP" 49 mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" 50 51 cp "$BIN" "$APP/Contents/MacOS/navi" 52 chmod +x "$APP/Contents/MacOS/navi" 53 strip -x "$APP/Contents/MacOS/navi" 2>/dev/null || true 54 55 # Icon — build a fully canonical .icns from the highest-resolution variant 56 # we can find, scaling with `sips` to every size Finder/Dock expects. We 57 # write the icns binary ourselves rather than going through iconutil because 58 # `iconutil` on recent macOS has been reported to reject perfectly valid 59 # iconsets with the "Invalid Iconset" error when files have any extended 60 # attributes the tool didn't write itself. 61 if [ -f assets/icon.icns ] || [ -f assets/icon.png ]; then 62 TMP_DIR="$(mktemp -d)" 63 PNG_DIR="$TMP_DIR/png" 64 mkdir -p "$PNG_DIR" 65 66 if [ -f assets/icon.png ]; then 67 SOURCE="assets/icon.png" 68 else 69 iconutil -c iconset -o "$TMP_DIR/extracted.iconset" assets/icon.icns >/dev/null 2>&1 70 if [ -f "$TMP_DIR/extracted.iconset/icon_512x512@2x.png" ]; then 71 SOURCE="$TMP_DIR/extracted.iconset/icon_512x512@2x.png" 72 elif [ -f "$TMP_DIR/extracted.iconset/icon_512x512.png" ]; then 73 SOURCE="$TMP_DIR/extracted.iconset/icon_512x512.png" 74 else 75 SOURCE="" 76 fi 77 fi 78 79 if [ -n "$SOURCE" ]; then 80 # Render every variant size as a clean PNG via sips. 81 for spec in 16:16.png 32:32.png 64:64.png 128:128.png 256:256.png \ 82 512:512.png 1024:1024.png; do 83 px="${spec%%:*}"; name="${spec##*:}" 84 sips -z "$px" "$px" -s format png --out "$PNG_DIR/$name" "$SOURCE" >/dev/null 85 done 86 87 # Pack into a canonical icns. The chunk-type → pixel-size mapping is 88 # the standard one Apple's docs document (icp4 = 16, icp5 = 32, etc.). 89 /usr/bin/python3 - "$PNG_DIR" "$APP/Contents/Resources/AppIcon.icns" <<'PY' 90 import os, struct, sys 91 src, out = sys.argv[1], sys.argv[2] 92 chunks = [ 93 ('icp4', '16.png'), # 16x16 94 ('icp5', '32.png'), # 32x32 95 ('icp6', '64.png'), # 64x64 96 ('ic07', '128.png'), # 128x128 97 ('ic08', '256.png'), # 256x256 98 ('ic09', '512.png'), # 512x512 99 ('ic10', '1024.png'), # 512x512@2x 100 ('ic11', '32.png'), # 16x16@2x 101 ('ic12', '64.png'), # 32x32@2x 102 ('ic13', '256.png'), # 128x128@2x 103 ('ic14', '512.png'), # 256x256@2x 104 ] 105 body = b'' 106 for code, fname in chunks: 107 data = open(os.path.join(src, fname), 'rb').read() 108 body += code.encode('ascii') + struct.pack('>I', len(data) + 8) + data 109 total = len(body) + 8 110 header = b'icns' + struct.pack('>I', total) 111 open(out, 'wb').write(header + body) 112 print(f" icns: {len(chunks)} chunks, {total} bytes -> {out}") 113 PY 114 else 115 cp assets/icon.icns "$APP/Contents/Resources/AppIcon.icns" 116 fi 117 rm -rf "$TMP_DIR" 118 fi 119 120 cat > "$APP/Contents/Info.plist" <<PLIST 121 <?xml version="1.0" encoding="UTF-8"?> 122 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 123 <plist version="1.0"> 124 <dict> 125 <key>CFBundleName</key> <string>Navi</string> 126 <key>CFBundleDisplayName</key> <string>Navi</string> 127 <key>CFBundleExecutable</key> <string>navi</string> 128 <key>CFBundleIconFile</key> <string>AppIcon</string> 129 <key>CFBundleIconName</key> <string>AppIcon</string> 130 <key>CFBundleIdentifier</key> <string>com.navi.graph</string> 131 <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> 132 <key>CFBundlePackageType</key> <string>APPL</string> 133 <key>CFBundleShortVersionString</key> <string>${VERSION}</string> 134 <key>CFBundleVersion</key> <string>${VERSION}</string> 135 <key>LSMinimumSystemVersion</key> <string>11.0</string> 136 <key>NSHighResolutionCapable</key> <true/> 137 <key>LSApplicationCategoryType</key> <string>public.app-category.utilities</string> 138 <key>NSPrincipalClass</key> <string>NSApplication</string> 139 </dict> 140 </plist> 141 PLIST 142 143 # Ad-hoc codesign so macOS lets the user open it (still flagged by Gatekeeper 144 # on first launch — instruct users to right-click → Open or `xattr -cr Navi.app`). 145 codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || \ 146 echo "warn: codesign failed (continuing, app will trip Gatekeeper)" 147 148 # Bump bundle mtime so Icon Services notices a fresh app and reloads the icon. 149 touch "$APP" "$APP/Contents/Info.plist" 150 151 # ── Package ────────────────────────────────────────────────────────────────── 152 echo "==> Packaging $ZIP" 153 ditto -c -k --sequesterRsrc --keepParent "$APP" "$ZIP" 154 155 du -h "$BIN" "$APP/Contents/MacOS/navi" "$ZIP" | sed 's/^/ /' 156 echo "==> Done. Open with: open $APP" 157 echo "==> Upload to release: gh release upload v${VERSION} $ZIP --clobber"