From 7927940c3810ee34640803b198d334a6ac93474d Mon Sep 17 00:00:00 2001 From: yunggilja Date: Sun, 31 May 2026 21:08:43 -0500 Subject: [PATCH] Add downloadable macOS launcher app builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build-macos-app.sh generates dist/Odysseus.app and a drag-to-Applications dist/Odysseus.dmg. The app starts the local server from this repo's venv and opens the UI in a chrome-less app window (Chromium --app mode, falling back to the default browser). It's a launcher wrapper — it drives the venv rather than bundling Python — so the install path is baked in at build time. Co-Authored-By: Claude Opus 4.8 --- README.md | 8 +++ build-macos-app.sh | 164 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100755 build-macos-app.sh diff --git a/README.md b/README.md index f731beb63..e80980bf2 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,14 @@ equivalent of the systemd unit), use the bundled launchd installer: ODYSSEUS_PORT=7860 ./install-service-macos.sh # if 7000 is taken by AirPlay ``` +Prefer a clickable app? Build a launcher `Odysseus.app` (+ a drag-to-Applications +`.dmg`) that starts the local server and opens the UI in its own window: +```bash +./build-macos-app.sh # → dist/Odysseus.app and dist/Odysseus.dmg +``` +This wraps the venv in this repo (it doesn't bundle Python), so the install path +is baked in at build time — rebuild if you move the repo. + ### Option 3: Manual install — Windows (PowerShell) Windows support is not actively tested. Use it with caution; Docker on Linux or a Linux/macOS manual install is the safer path for now. diff --git a/build-macos-app.sh b/build-macos-app.sh new file mode 100755 index 000000000..7c9ba87f8 --- /dev/null +++ b/build-macos-app.sh @@ -0,0 +1,164 @@ +#!/bin/bash +# Build a downloadable macOS launcher app + .dmg for Odysseus. +# +# ./build-macos-app.sh +# +# Produces: +# dist/Odysseus.app — double-click: starts the local server (using this +# repo's venv) and opens the UI in an app-style window. +# dist/Odysseus.dmg — drag-to-Applications disk image (the downloadable). +# +# This is a *launcher* wrapper: it drives the venv we set up in this repo, it +# does not bundle Python. The install path is baked into the app at build time, +# so rebuild if you move the repo. Override the port with ODYSSEUS_PORT. +set -e + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_NAME="Odysseus" +INSTALL_DIR="$REPO_DIR" +PORT="${ODYSSEUS_PORT:-7860}" +DIST="$REPO_DIR/dist" +APP="$DIST/$APP_NAME.app" + +echo "Building $APP_NAME.app" +echo " install dir: $INSTALL_DIR" +echo " port: $PORT" + +rm -rf "$APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" + +# ── Icon (best effort) — center-crop docs/odysseus.jpg to a square .icns ── +if [ -f "$REPO_DIR/docs/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then + TMPIMG="$(mktemp -d)" + # Center-crop to a square, scale to 512 (sips' icns encoder caps at 512), and + # let sips emit the .icns directly — more robust across macOS versions than + # building an .iconset by hand. + sips -c 720 720 "$REPO_DIR/docs/odysseus.jpg" --out "$TMPIMG/sq.png" >/dev/null 2>&1 || cp "$REPO_DIR/docs/odysseus.jpg" "$TMPIMG/sq.png" + sips -z 512 512 "$TMPIMG/sq.png" --out "$TMPIMG/icon.png" >/dev/null 2>&1 + if sips -s format icns "$TMPIMG/icon.png" --out "$APP/Contents/Resources/odysseus.icns" >/dev/null 2>&1; then + echo " icon: odysseus.icns" + else + echo " icon: (skipped — conversion failed)" + fi + rm -rf "$TMPIMG" +else + echo " icon: (skipped — no docs/odysseus.jpg)" +fi + +# ── Info.plist ── +cat > "$APP/Contents/Info.plist" < + + + + CFBundleName $APP_NAME + CFBundleDisplayName $APP_NAME + CFBundleIdentifier com.odysseus.launcher + CFBundleVersion 1.0 + CFBundleShortVersionString1.0 + CFBundlePackageType APPL + CFBundleExecutable $APP_NAME + CFBundleIconFile odysseus + LSMinimumSystemVersion 11.0 + NSHighResolutionCapable + LSUIElement + + +PLIST + +# ── Launcher executable (placeholders filled below) ── +cat > "$APP/Contents/MacOS/$APP_NAME.tmpl" <<'LAUNCHER' +#!/bin/bash +# Odysseus.app — start the local server and open the UI in an app window. +INSTALL_DIR="__INSTALL_DIR__" +PORT="__PORT__" +URL="http://127.0.0.1:${PORT}" +export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH" + +UVICORN="$INSTALL_DIR/venv/bin/uvicorn" +LOG="$INSTALL_DIR/logs/odysseus-app.log" + +notify() { /usr/bin/osascript -e "display notification \"$1\" with title \"Odysseus\"" >/dev/null 2>&1; } +die_gui() { + /usr/bin/osascript -e "display dialog \"$1\" with title \"Odysseus\" buttons {\"OK\"} default button 1 with icon stop" >/dev/null 2>&1 + exit 1 +} + +[ -x "$UVICORN" ] || die_gui "Odysseus isn't set up yet. Open Terminal and run: + +cd $INSTALL_DIR +python3.11 -m venv venv +./venv/bin/pip install -r requirements.txt +./venv/bin/python setup.py" + +# Open the UI in a chrome-less app window (Chromium browsers), else default browser. +open_ui() { + local b base exe bin + for b in "Google Chrome" "Microsoft Edge" "Brave Browser" "Chromium"; do + for base in "/Applications" "$HOME/Applications"; do + if [ -d "$base/$b.app" ]; then + exe="$(/usr/bin/defaults read "$base/$b.app/Contents/Info" CFBundleExecutable 2>/dev/null)" + bin="$base/$b.app/Contents/MacOS/$exe" + if [ -x "$bin" ]; then + "$bin" --app="$URL" --new-window >/dev/null 2>&1 & + return 0 + fi + fi + done + done + /usr/bin/open "$URL" +} + +mkdir -p "$INSTALL_DIR/logs" + +# Already running? Just open the UI. +if /usr/bin/curl -s -o /dev/null --max-time 2 "$URL"; then + open_ui + exit 0 +fi + +notify "Starting…" +cd "$INSTALL_DIR" || die_gui "Install folder not found: $INSTALL_DIR" +"$UVICORN" app:app --host 127.0.0.1 --port "$PORT" >>"$LOG" 2>&1 & +SERVER_PID=$! + +# Quitting the app stops the server it started. +trap 'kill $SERVER_PID 2>/dev/null; exit 0' TERM INT + +# Wait for readiness (first run downloads an embedding model — allow ~2 min). +for i in $(seq 1 120); do + /usr/bin/curl -s -o /dev/null --max-time 2 "$URL" && break + kill -0 "$SERVER_PID" 2>/dev/null || die_gui "Odysseus failed to start. Log: +$LOG" + sleep 1 +done + +open_ui +wait "$SERVER_PID" +LAUNCHER + +sed -e "s|__INSTALL_DIR__|$INSTALL_DIR|g" -e "s|__PORT__|$PORT|g" \ + "$APP/Contents/MacOS/$APP_NAME.tmpl" > "$APP/Contents/MacOS/$APP_NAME" +rm -f "$APP/Contents/MacOS/$APP_NAME.tmpl" +chmod +x "$APP/Contents/MacOS/$APP_NAME" + +# Refresh Finder's icon cache for the new bundle. +touch "$APP" + +# ── .dmg (drag-to-Applications) ── +echo "Packaging dist/$APP_NAME.dmg" +STAGE="$(mktemp -d)/dmg" +mkdir -p "$STAGE" +cp -R "$APP" "$STAGE/" +ln -s /Applications "$STAGE/Applications" +rm -f "$DIST/$APP_NAME.dmg" +hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO "$DIST/$APP_NAME.dmg" >/dev/null +rm -rf "$STAGE" + +echo "" +echo "Done:" +echo " $APP" +echo " $DIST/$APP_NAME.dmg" +echo "" +echo "Run it: open '$APP'" +echo "Install: open '$DIST/$APP_NAME.dmg' (drag Odysseus to Applications)"