Created
November 17, 2025 02:30
-
-
Save ahalekelly/c270b7a3584c398af74b103065e1eb5e to your computer and use it in GitHub Desktop.
Create Mac App using py-app-standalone
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Example build script showing how to wrap py-app-standalone output into macOS .app bundles | |
| # This provides instant startup times with native macOS application experience | |
| set -e # Exit on error | |
| echo "Building standalone executables..." | |
| echo "Platform: $(uname -s)" | |
| echo "" | |
| # Check if uv is installed | |
| if ! command -v uv &> /dev/null; then | |
| echo "Error: uv is not installed. Please install it first:" | |
| echo " curl -LsSf https://astral.sh/uv/install.sh | sh" | |
| exit 1 | |
| fi | |
| echo "Using uv version: $(uv --version)" | |
| echo "" | |
| # Clean previous builds | |
| echo "Cleaning previous builds..." | |
| rm -rf py-standalone | |
| # Build the standalone distribution from local package | |
| echo "Building standalone Python environment from local package..." | |
| uvx py-app-standalone . --source-only | |
| # On macOS, create .app bundles | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| echo "" | |
| echo "Creating macOS .app bundles..." | |
| # Find the Python installation directory | |
| PYTHON_DIR=$(find py-standalone -maxdepth 1 -name "cpython-*" -type d | head -1) | |
| if [ -z "$PYTHON_DIR" ]; then | |
| echo "Error: Could not find Python installation directory" | |
| exit 1 | |
| fi | |
| # Function to create .app bundle | |
| # This wraps the py-app-standalone output in a native macOS application structure | |
| # Benefits: | |
| # - Native macOS experience (can be launched from Finder, appears in Dock, etc.) | |
| # - Instant startup (no extraction like PyInstaller --onefile) | |
| # - No Gatekeeper issues with nested executables (single entry point) | |
| create_app_bundle() { | |
| local APP_NAME="$1" # Display name (e.g., "My App") | |
| local EXECUTABLE_NAME="$2" # Entry point script name from pyproject.toml [project.scripts] | |
| local BUNDLE_ID="$3" # Reverse DNS bundle identifier (e.g., "com.example.myapp") | |
| echo "Creating $APP_NAME.app..." | |
| # Create .app structure (standard macOS application bundle layout) | |
| mkdir -p "dist/$APP_NAME.app/Contents/MacOS" | |
| mkdir -p "dist/$APP_NAME.app/Contents/Resources" | |
| # Copy the entire py-standalone directory into Resources | |
| # This includes the Python interpreter, libraries, and all dependencies | |
| cp -R py-standalone "dist/$APP_NAME.app/Contents/Resources/" | |
| # Create launcher script that executes the py-app-standalone entry point | |
| cat > "dist/$APP_NAME.app/Contents/MacOS/$APP_NAME" << 'EOF' | |
| #!/bin/bash | |
| # Get the directory where this script is located | |
| DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | |
| # Run the actual Python executable (entry point from pyproject.toml [project.scripts]) | |
| exec "$DIR/../Resources/py-standalone/cpython-"*"/bin/EXEC_NAME" "$@" | |
| EOF | |
| # Replace EXEC_NAME with actual executable name | |
| sed -i '' "s/EXEC_NAME/$EXECUTABLE_NAME/g" "dist/$APP_NAME.app/Contents/MacOS/$APP_NAME" | |
| chmod +x "dist/$APP_NAME.app/Contents/MacOS/$APP_NAME" | |
| # Create Info.plist (macOS application metadata) | |
| cat > "dist/$APP_NAME.app/Contents/Info.plist" << EOF | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>CFBundleExecutable</key> | |
| <string>$APP_NAME</string> | |
| <key>CFBundleIdentifier</key> | |
| <string>$BUNDLE_ID</string> | |
| <key>CFBundleName</key> | |
| <string>$APP_NAME</string> | |
| <key>CFBundlePackageType</key> | |
| <string>APPL</string> | |
| <key>CFBundleShortVersionString</key> | |
| <string>0.1.0</string> | |
| <key>CFBundleVersion</key> | |
| <string>1</string> | |
| <key>LSMinimumSystemVersion</key> | |
| <string>10.13</string> | |
| <key>NSHighResolutionCapable</key> | |
| <true/> | |
| </dict> | |
| </plist> | |
| EOF | |
| } | |
| # Create dist directory | |
| mkdir -p dist | |
| # Create .app bundle for the entry point defined in pyproject.toml [project.scripts] | |
| # Example: If you have [project.scripts] with: | |
| # my-app = "mypackage.main:run" | |
| # Then create bundle like: | |
| create_app_bundle "My App" "my-app" "com.example.myapp" | |
| echo "" | |
| echo "macOS .app bundle created in dist/" | |
| fi | |
| echo "" | |
| echo "Build complete!" | |
| echo "" | |
| if [[ "$OSTYPE" == "darwin"* ]]; then | |
| echo "macOS .app bundle is located in:" | |
| echo " ./dist/" | |
| echo "" | |
| echo "Available applications:" | |
| ls -d dist/*.app | |
| echo "" | |
| echo "Double-click the .app file to launch, or run:" | |
| echo " open 'dist/My App.app'" | |
| else | |
| echo "The standalone executables are located in:" | |
| echo " ./py-standalone/cpython-*/bin/" | |
| echo "" | |
| echo "Available executables:" | |
| find py-standalone -name "my-app" | |
| echo "" | |
| echo "You can move the entire py-standalone directory to any compatible system." | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment