Skill Tracer
Skills › gstack › gstack › build-app.sh

build-app.sh

211 lines · scripts/build-app.sh at 3f79d81

1#!/bin/bash
2# Build GStack Browser.app — macOS application bundle
3#
4# Creates a self-contained .app with:
5# - Compiled browse binary
6# - Playwright's bundled Chromium
7# - Chrome extension (sidebar)
8# - Info.plist with bundle ID
9#
10# Output: dist/GStack Browser.app and dist/GStack-Browser.dmg
12# Usage:
13# ./scripts/build-app.sh # Build .app + DMG
14# ./scripts/build-app.sh --no-dmg # Build .app only
16# Heredoc delivery guard. bash 5.2+ writes a heredoc body <=64KiB through a
17# pipe in the forked child before exec, with no reader on the other end. On
18# macOS under pipe-KVA pressure a fresh pipe gets a 512-byte buffer, so any
19# body >=512B blocks write() forever and the script hangs at startup with no
20# output. Compat level 50 restores the tempfile path. These scripts are
21# bash-3.2-clean, so the compat level costs them nothing. Not exported: the
22# guard is per-script, and it survives `bash script.sh` call sites that
23# bypass the shebang.
24BASH_COMPAT=50
26set -euo pipefail
28SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
29ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
30APP_NAME="GStack Browser"
31BUNDLE_ID="com.gstack.browser"
32VERSION=$(cat "$ROOT/VERSION" 2>/dev/null || echo "0.0.1")
33BUILD_DIR="$ROOT/dist"
34APP_DIR="$BUILD_DIR/$APP_NAME.app"
36echo "Building $APP_NAME v$VERSION..."
38# ─── Step 1: Compile browse binary ─────────────────────────────
39echo " Compiling browse binary..."
40cd "$ROOT/browse"
41bun build --compile --no-compile-autoload-dotenv --no-compile-autoload-bunfig src/cli.ts --outfile "$BUILD_DIR/browse-app" --target=bun 2>/dev/null
42cd "$ROOT"
44# ─── Step 2: Find Playwright's Chromium ─────────────────────────
45echo " Locating Playwright Chromium..."
46PW_CACHE="$HOME/Library/Caches/ms-playwright"
47CHROMIUM_DIR=$(ls -d "$PW_CACHE"/chromium-*/chrome-mac-arm64 2>/dev/null | sort -V | tail -1)
49if [ -z "$CHROMIUM_DIR" ]; then
50 echo "ERROR: Playwright Chromium not found in $PW_CACHE"
51 echo "Run: bunx playwright install chromium"
52 exit 1
53fi
55CHROME_APP=$(ls -d "$CHROMIUM_DIR"/*.app 2>/dev/null | head -1)
56if [ -z "$CHROME_APP" ]; then
57 echo "ERROR: Chrome .app not found in $CHROMIUM_DIR"
58 exit 1
59fi
60echo " Found: $(basename "$CHROME_APP")"
62# ─── Step 3: Create .app structure ──────────────────────────────
63echo " Building .app bundle..."
64rm -rf "$APP_DIR"
65mkdir -p "$APP_DIR/Contents/MacOS"
66mkdir -p "$APP_DIR/Contents/Resources"
68# Launcher script
69cp "$ROOT/scripts/app/gstack-browser" "$APP_DIR/Contents/MacOS/gstack-browser"
70chmod +x "$APP_DIR/Contents/MacOS/gstack-browser"
72# Browse binary
73cp "$BUILD_DIR/browse-app" "$APP_DIR/Contents/Resources/browse"
74chmod +x "$APP_DIR/Contents/Resources/browse"
76# Extension
77cp -r "$ROOT/extension" "$APP_DIR/Contents/Resources/extension"
78# Remove .auth.json if present (auth now via /health endpoint)
79rm -f "$APP_DIR/Contents/Resources/extension/.auth.json"
81# Server source (needed for `bun run server.ts` subprocess)
82# The launcher sets BROWSE_SERVER_SCRIPT to point at this.
83# Copy the full src/ directory since server.ts imports other modules.
84echo " Copying browse source..."
85cp -r "$ROOT/browse/src" "$APP_DIR/Contents/Resources/src"
86# Also need package.json for module resolution
87cp "$ROOT/browse/package.json" "$APP_DIR/Contents/Resources/" 2>/dev/null || true
89# Chromium
90mkdir -p "$APP_DIR/Contents/Resources/chromium"
91echo " Copying Chromium (~330MB)..."
92cp -a "$CHROME_APP" "$APP_DIR/Contents/Resources/chromium/"
94# ─── Step 3b: Rebrand Chromium ────────────────────────────────────
95# Patch the bundled Chromium's Info.plist so macOS shows "GStack Browser"
96# in the menu bar, Dock, and Cmd+Tab instead of "Google Chrome for Testing"
97CHROMIUM_PLIST="$APP_DIR/Contents/Resources/chromium/$(basename "$CHROME_APP")/Contents/Info.plist"
98if [ -f "$CHROMIUM_PLIST" ]; then
99 echo " Rebranding Chromium → $APP_NAME..."
100 /usr/libexec/PlistBuddy -c "Set :CFBundleName '$APP_NAME'" "$CHROMIUM_PLIST"
101 /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName '$APP_NAME'" "$CHROMIUM_PLIST"
102 # Also update the localized strings if present
103 CHROMIUM_STRINGS="$APP_DIR/Contents/Resources/chromium/$(basename "$CHROME_APP")/Contents/Resources/en.lproj/InfoPlist.strings"
104 if [ -f "$CHROMIUM_STRINGS" ]; then
105 # InfoPlist.strings may be binary plist, convert to xml first
106 plutil -convert xml1 "$CHROMIUM_STRINGS" 2>/dev/null || true
107 # Escape sed replacement metachars (& / \) in $APP_NAME so unusual names can't break or inject into the s/// command.
108 APP_NAME_SED_ESCAPED=$(printf '%s' "$APP_NAME" | sed 's/[&/\]/\\&/g')
109 sed -i '' "s/Google Chrome for Testing/${APP_NAME_SED_ESCAPED}/g" "$CHROMIUM_STRINGS" 2>/dev/null || true
110 fi
111 # Replace Chromium's icon with ours so the Dock shows the GStack icon
112 # (Chromium's process owns the Dock icon, not our launcher)
113 ICON_SRC="$SCRIPT_DIR/app/icon.icns"
114 if [ -f "$ICON_SRC" ]; then
115 CHROMIUM_RESOURCES="$APP_DIR/Contents/Resources/chromium/$(basename "$CHROME_APP")/Contents/Resources"
116 # Find the original icon filename from Chromium's plist
117 ORIG_ICON=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIconFile" "$CHROMIUM_PLIST" 2>/dev/null || echo "app")
118 # Add .icns extension if not present
119 [[ "$ORIG_ICON" != *.icns ]] && ORIG_ICON="${ORIG_ICON}.icns"
120 cp "$ICON_SRC" "$CHROMIUM_RESOURCES/$ORIG_ICON"
121 echo " Replaced Chromium icon → $ORIG_ICON"
122 fi
123fi
125# ─── Step 3c: App icon ────────────────────────────────────────────
126ICON_SRC="$SCRIPT_DIR/app/icon.icns"
127if [ -f "$ICON_SRC" ]; then
128 cp "$ICON_SRC" "$APP_DIR/Contents/Resources/icon.icns"
129 echo " App icon installed"
130else
131 echo " WARNING: No icon.icns found at $ICON_SRC — app will use default icon"
132fi
134# ─── Step 4: Info.plist ──────────────────────────────────────────
135cat > "$APP_DIR/Contents/Info.plist" << PLIST
136<?xml version="1.0" encoding="UTF-8"?>
137<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
138<plist version="1.0">
139<dict>
140 <key>CFBundleName</key>
141 <string>$APP_NAME</string>
142 <key>CFBundleDisplayName</key>
143 <string>$APP_NAME</string>
144 <key>CFBundleIdentifier</key>
145 <string>$BUNDLE_ID</string>
146 <key>CFBundleVersion</key>
147 <string>$VERSION</string>
148 <key>CFBundleShortVersionString</key>
149 <string>$VERSION</string>
150 <key>CFBundleExecutable</key>
151 <string>gstack-browser</string>
152 <key>CFBundlePackageType</key>
153 <string>APPL</string>
154 <key>CFBundleSignature</key>
155 <string>????</string>
156 <key>LSMinimumSystemVersion</key>
157 <string>12.0</string>
158 <key>CFBundleIconFile</key>
159 <string>icon</string>
160 <key>NSHighResolutionCapable</key>
161 <true/>
162 <key>LSApplicationCategoryType</key>
163 <string>public.app-category.developer-tools</string>
164 <key>NSSupportsAutomaticTermination</key>
165 <false/>
166</dict>
167</plist>
168PLIST
170# ─── Step 5: App size report ────────────────────────────────────
171APP_SIZE=$(du -sh "$APP_DIR" | cut -f1)
172echo ""
173echo " $APP_NAME.app: $APP_SIZE"
174echo " Contents/MacOS/gstack-browser (launcher)"
175echo " Contents/Resources/browse ($(du -sh "$APP_DIR/Contents/Resources/browse" | cut -f1))"
176echo " Contents/Resources/extension/ ($(du -sh "$APP_DIR/Contents/Resources/extension" | cut -f1))"
177echo " Contents/Resources/chromium/ ($(du -sh "$APP_DIR/Contents/Resources/chromium" | cut -f1))"
179# ─── Step 6: DMG (optional) ─────────────────────────────────────
180if [ "${1:-}" = "--no-dmg" ]; then
181 echo ""
182 echo "Done. App at: $APP_DIR"
183 exit 0
184fi
186DMG_PATH="$BUILD_DIR/GStack-Browser.dmg"
187echo ""
188echo " Creating DMG..."
189rm -f "$DMG_PATH"
191# Create a temporary directory for DMG contents
192DMG_TMP=$(mktemp -d) || { echo "ERROR: mktemp -d failed — refusing to continue so we don't cp into the filesystem root." >&2; exit 1; }
193if [ -z "$DMG_TMP" ] || [ ! -d "$DMG_TMP" ]; then
194 echo "ERROR: mktemp -d returned an invalid path ('$DMG_TMP')." >&2
195 exit 1
196fi
197cp -a "$APP_DIR" "$DMG_TMP/"
198ln -s /Applications "$DMG_TMP/Applications"
200hdiutil create -volname "$APP_NAME" \
201 -srcfolder "$DMG_TMP" \
202 -ov -format UDZO \
203 "$DMG_PATH" \
204 > /dev/null 2>&1
206rm -rf "$DMG_TMP"
208DMG_SIZE=$(du -sh "$DMG_PATH" | cut -f1)
209echo " DMG: $DMG_SIZE → $DMG_PATH"
210echo ""
211echo "Done. Install: open $DMG_PATH"