navi

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

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:
Mscripts/build-macos.sh | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
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"