navi

Obsidian-style interactive graph viewer for org-roam — native window, no Emacs package required.
Log | Files | Refs | README

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"