#!/bin/bash # # Build script for Path Juggler # Creates a standalone macOS .app bundle using Nuitka # Uses PySide6 (Qt) for GUI - fully bundleable, no system dependencies # set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" APP_NAME="Path Juggler" BUNDLE_ID="com.pathjuggler.app" VENV_DIR="$SCRIPT_DIR/.build_venv" DIST_DIR="$SCRIPT_DIR/dist" echo "=== Path Juggler - Build Script ===" echo "Using Nuitka + PySide6 for a fully self-contained build" echo "" # Check for uv if ! command -v uv &> /dev/null; then echo "Error: uv is required but not found" echo "" echo "Install uv with:" echo " curl -LsSf https://astral.sh/uv/install.sh | sh" echo " or: brew install uv" exit 1 fi echo "Using uv version: $(uv --version)" echo "" # Create virtual environment with Python 3.11 (good compatibility) echo "=== Creating virtual environment ===" if [ -d "$VENV_DIR" ]; then echo "Removing existing build environment..." rm -rf "$VENV_DIR" fi uv venv "$VENV_DIR" --python 3.11 source "$VENV_DIR/bin/activate" PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') echo "Python version: $PYTHON_VERSION" # Install build dependencies echo "" echo "=== Installing build dependencies ===" uv pip install --quiet PySide6 watchdog nuitka ordered-set zstandard echo "Installed packages:" uv pip list | grep -iE "(pyside6|watchdog|nuitka)" || true # Build with Nuitka echo "" echo "=== Building application with Nuitka ===" # Clean previous build rm -rf "$DIST_DIR/$APP_NAME.app" 2>/dev/null || true rm -rf "$DIST_DIR"/*.build 2>/dev/null || true rm -rf "$DIST_DIR"/*.dist 2>/dev/null || true python -m nuitka \ --standalone \ --macos-create-app-bundle \ --macos-app-name="$APP_NAME" \ --product-name="$APP_NAME" \ --product-version="1.0.0" \ --company-name="PathJuggler" \ --file-description="File organization utility" \ --enable-plugin=pyside6 \ --include-module=watchdog.observers \ --include-module=watchdog.observers.fsevents \ --include-module=watchdog.events \ --output-dir="$DIST_DIR" \ --remove-output \ --assume-yes-for-downloads \ path_juggler_gui.py # Find the generated app APP_PATH="$DIST_DIR/path_juggler_gui.app" FINAL_APP_PATH="$DIST_DIR/$APP_NAME.app" if [ -d "$APP_PATH" ]; then # Rename to proper name rm -rf "$FINAL_APP_PATH" 2>/dev/null || true mv "$APP_PATH" "$FINAL_APP_PATH" # Update Info.plist with proper bundle ID PLIST="$FINAL_APP_PATH/Contents/Info.plist" if [ -f "$PLIST" ]; then /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$PLIST" 2>/dev/null || true /usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$PLIST" 2>/dev/null || true /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $APP_NAME" "$PLIST" 2>/dev/null || true fi # Sign the app echo "" echo "=== Signing application (ad-hoc) ===" codesign --remove-signature "$FINAL_APP_PATH" 2>/dev/null || true codesign --force --deep --sign - "$FINAL_APP_PATH" echo "Signature applied" codesign -dv "$FINAL_APP_PATH" 2>&1 | head -5 else echo "Error: App bundle not found" echo "Looking for: $APP_PATH" ls -la "$DIST_DIR/" exit 1 fi # Clean up echo "" echo "=== Cleaning up ===" rm -rf "$DIST_DIR"/*.build 2>/dev/null || true rm -rf "$DIST_DIR"/*.dist 2>/dev/null || true rm -rf "$DIST_DIR"/*.onefile-build 2>/dev/null || true deactivate rm -rf "$VENV_DIR" echo "" echo "=== Build complete ===" echo "" echo "Application created at:" echo " $FINAL_APP_PATH" echo "" echo "To install, drag the app to your Applications folder:" echo " open \"$DIST_DIR\"" echo "" echo "Note: On first launch, you may need to right-click and select 'Open'" echo "to bypass Gatekeeper since this is a self-signed app."