commit b53528f571c9f882d57ec36870c621757ee7aab8
parent 2d4b3fbdf88717326e4bfbd221a96f6cd98c9b51
Author: Matthew Gantenbein <ganten@Matthews-MBP.ht.home>
Date: Wed, 20 May 2026 23:12:31 -0500
build-macos.sh: hand-roll canonical icns to fix missing app icon
The recovered assets/icon.icns from the previous Python release was missing
several @2x retina sub-icons. Icon Services silently falls back to the
generic icon when it can't find the variant for the current display, which
is why Navi.app showed no icon in Finder/Dock.
Replaced the iconutil pipeline (which on recent macOS rejects perfectly
valid iconsets with 'Invalid Iconset' due to xattrs / sandbox provenance)
with a direct icns writer in Python. Now produces all 11 standard chunks:
icp4/icp5/icp6 (16/32/64) + ic07-ic10 (128/256/512/1024) + ic11-ic14
(@2x retina). Verified by round-tripping through iconutil -c iconset.
Co-authored-by: Cursor <cursoragent@cursor.com>
Diffstat:
1 file changed, 67 insertions(+), 2 deletions(-)
diff --git a/scripts/build-macos.sh b/scripts/build-macos.sh
@@ -52,8 +52,69 @@ cp "$BIN" "$APP/Contents/MacOS/navi"
chmod +x "$APP/Contents/MacOS/navi"
strip -x "$APP/Contents/MacOS/navi" 2>/dev/null || true
-if [ -f assets/icon.icns ]; then
- cp assets/icon.icns "$APP/Contents/Resources/AppIcon.icns"
+# Icon — build a fully canonical .icns from the highest-resolution variant
+# we can find, scaling with `sips` to every size Finder/Dock expects. We
+# write the icns binary ourselves rather than going through iconutil because
+# `iconutil` on recent macOS has been reported to reject perfectly valid
+# iconsets with the "Invalid Iconset" error when files have any extended
+# attributes the tool didn't write itself.
+if [ -f assets/icon.icns ] || [ -f assets/icon.png ]; then
+ TMP_DIR="$(mktemp -d)"
+ PNG_DIR="$TMP_DIR/png"
+ mkdir -p "$PNG_DIR"
+
+ if [ -f assets/icon.png ]; then
+ SOURCE="assets/icon.png"
+ else
+ iconutil -c iconset -o "$TMP_DIR/extracted.iconset" assets/icon.icns >/dev/null 2>&1
+ if [ -f "$TMP_DIR/extracted.iconset/icon_512x512@2x.png" ]; then
+ SOURCE="$TMP_DIR/extracted.iconset/icon_512x512@2x.png"
+ elif [ -f "$TMP_DIR/extracted.iconset/icon_512x512.png" ]; then
+ SOURCE="$TMP_DIR/extracted.iconset/icon_512x512.png"
+ else
+ SOURCE=""
+ fi
+ fi
+
+ if [ -n "$SOURCE" ]; then
+ # Render every variant size as a clean PNG via sips.
+ for spec in 16:16.png 32:32.png 64:64.png 128:128.png 256:256.png \
+ 512:512.png 1024:1024.png; do
+ px="${spec%%:*}"; name="${spec##*:}"
+ sips -z "$px" "$px" -s format png --out "$PNG_DIR/$name" "$SOURCE" >/dev/null
+ done
+
+ # Pack into a canonical icns. The chunk-type → pixel-size mapping is
+ # the standard one Apple's docs document (icp4 = 16, icp5 = 32, etc.).
+ /usr/bin/python3 - "$PNG_DIR" "$APP/Contents/Resources/AppIcon.icns" <<'PY'
+import os, struct, sys
+src, out = sys.argv[1], sys.argv[2]
+chunks = [
+ ('icp4', '16.png'), # 16x16
+ ('icp5', '32.png'), # 32x32
+ ('icp6', '64.png'), # 64x64
+ ('ic07', '128.png'), # 128x128
+ ('ic08', '256.png'), # 256x256
+ ('ic09', '512.png'), # 512x512
+ ('ic10', '1024.png'), # 512x512@2x
+ ('ic11', '32.png'), # 16x16@2x
+ ('ic12', '64.png'), # 32x32@2x
+ ('ic13', '256.png'), # 128x128@2x
+ ('ic14', '512.png'), # 256x256@2x
+]
+body = b''
+for code, fname in chunks:
+ data = open(os.path.join(src, fname), 'rb').read()
+ body += code.encode('ascii') + struct.pack('>I', len(data) + 8) + data
+total = len(body) + 8
+header = b'icns' + struct.pack('>I', total)
+open(out, 'wb').write(header + body)
+print(f" icns: {len(chunks)} chunks, {total} bytes -> {out}")
+PY
+ else
+ cp assets/icon.icns "$APP/Contents/Resources/AppIcon.icns"
+ fi
+ rm -rf "$TMP_DIR"
fi
cat > "$APP/Contents/Info.plist" <<PLIST
@@ -65,6 +126,7 @@ cat > "$APP/Contents/Info.plist" <<PLIST
<key>CFBundleDisplayName</key> <string>Navi</string>
<key>CFBundleExecutable</key> <string>navi</string>
<key>CFBundleIconFile</key> <string>AppIcon</string>
+ <key>CFBundleIconName</key> <string>AppIcon</string>
<key>CFBundleIdentifier</key> <string>com.navi.graph</string>
<key>CFBundleInfoDictionaryVersion</key> <string>6.0</string>
<key>CFBundlePackageType</key> <string>APPL</string>
@@ -83,6 +145,9 @@ PLIST
codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || \
echo "warn: codesign failed (continuing, app will trip Gatekeeper)"
+# Bump bundle mtime so Icon Services notices a fresh app and reloads the icon.
+touch "$APP" "$APP/Contents/Info.plist"
+
# ── Package ──────────────────────────────────────────────────────────────────
echo "==> Packaging $ZIP"
ditto -c -k --sequesterRsrc --keepParent "$APP" "$ZIP"