Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/build-arcade-exe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Build Lemonade Arcade Executable

on:
push:
branches: [ main, develop ]
paths:
- '**'
- '.github/workflows/build-arcade-exe.yml'
pull_request:
branches: [ main ]
paths:
- '**'
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Version to build (e.g., 0.1.0)'
required: false
default: '0.1.0'

jobs:
build-executable:
runs-on: windows-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Cache Python dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install pyinstaller

- name: Build executable with PyInstaller
run: |
python -m PyInstaller lemonade_arcade.spec
shell: pwsh

- name: Verify build outputs
run: |
if (-not (Test-Path "dist\LemonadeArcade.exe")) {
Write-Error "Executable not found"
exit 1
}

# Show file information
$exe = Get-Item "dist\LemonadeArcade.exe"

Write-Host "Build artifact:"
Write-Host " - Executable: $($exe.Name) ($([math]::Round($exe.Length / 1MB, 2)) MB)"
Write-Host " - Location: $($exe.FullName)"
shell: pwsh

- name: Upload executable artifact
uses: actions/upload-artifact@v4
with:
name: lemonade-arcade-exe
path: dist/LemonadeArcade.exe
retention-days: 30

- name: Upload to release (if release)
if: github.event_name == 'release'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dist/LemonadeArcade.exe
asset_name: LemonadeArcade-${{ github.event.release.tag_name }}.exe
asset_content_type: application/octet-stream
63 changes: 63 additions & 0 deletions build_exe.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Lemonade Arcade Executable Build Script
# This script builds the Lemonade Arcade application using PyInstaller

param(
[switch]$Clean,
[string]$OutputDir = "dist"
)

Write-Host "Lemonade Arcade Executable Builder" -ForegroundColor Green
Write-Host "==================================" -ForegroundColor Green

# Get script directory
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
Set-Location $ScriptDir

# Function to run commands with error handling
function Invoke-Command-Safe {
param([string]$Command, [string]$ErrorMessage = "Command failed")

Write-Host "Running: $Command" -ForegroundColor Yellow
Invoke-Expression $Command

if ($LASTEXITCODE -ne 0) {
Write-Error $ErrorMessage
exit 1
}
}

# Clean previous builds if requested
if ($Clean) {
Write-Host "Cleaning previous builds..." -ForegroundColor Cyan
if (Test-Path "build") { Remove-Item -Recurse -Force "build" }
if (Test-Path "dist") { Remove-Item -Recurse -Force "dist" }
if (Test-Path "__pycache__") { Remove-Item -Recurse -Force "__pycache__" }
}

# Install dependencies
Write-Host "Installing build dependencies..." -ForegroundColor Cyan
Invoke-Command-Safe "python -m pip install --upgrade pip" "Failed to upgrade pip"
Invoke-Command-Safe "python -m pip install pyinstaller" "Failed to install PyInstaller"

# Build executable
Write-Host "Building executable with PyInstaller..." -ForegroundColor Cyan
Invoke-Command-Safe "python -m PyInstaller lemonade_arcade.spec" "Failed to build executable"

# Verify executable was created
$ExePath = "$OutputDir\LemonadeArcade.exe"
if (-not (Test-Path $ExePath)) {
Write-Error "Executable not found at $ExePath"
exit 1
}

# Show build results
$ExeFile = Get-Item $ExePath
Write-Host "" -ForegroundColor Green
Write-Host "Build completed successfully!" -ForegroundColor Green
Write-Host "Output file:" -ForegroundColor Green
Write-Host " - Executable: $(Resolve-Path $ExePath)" -ForegroundColor White
Write-Host " - Size: $([math]::Round($ExeFile.Length / 1MB, 2)) MB" -ForegroundColor White
Write-Host "" -ForegroundColor Green
Write-Host "To run the application:" -ForegroundColor Cyan
Write-Host " Double-click: $ExePath" -ForegroundColor White
Write-Host " Or from command line: $ExePath" -ForegroundColor White
33 changes: 33 additions & 0 deletions hook_pygame.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
import sys


# Runtime hook for pygame DLL loading in PyInstaller
def setup_pygame_environment():
"""Set up environment for pygame DLL loading in PyInstaller bundle."""
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
# We're in a PyInstaller bundle
bundle_dir = sys._MEIPASS

# Add the bundle directory to DLL search paths
if hasattr(os, "add_dll_directory"):
try:
os.add_dll_directory(bundle_dir)
except (OSError, FileNotFoundError):
pass

# Set environment variables that help with SDL2 loading and prevent SDL3
os.environ["SDL_VIDEODRIVER"] = "windib"
os.environ["SDL_AUDIODRIVER"] = "directsound"

# Explicitly prefer SDL2 over SDL3
os.environ["SDL_DYNAMIC_API"] = os.path.join(bundle_dir, "SDL2.dll")

# Add bundle to PATH for DLL resolution (put it first to prioritize our DLLs)
current_path = os.environ.get("PATH", "")
if bundle_dir not in current_path:
os.environ["PATH"] = bundle_dir + os.pathsep + current_path


# Run the setup immediately when this hook is imported
setup_pygame_environment()
Binary file added img/icon.ico
Binary file not shown.
120 changes: 120 additions & 0 deletions lemonade_arcade.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# -*- mode: python ; coding: utf-8 -*-

import os
from pathlib import Path

# Get the base directory - use current working directory instead of __file__
base_dir = Path(os.getcwd())

def find_pygame_binaries():
"""Find pygame DLLs automatically - works in any environment."""
binaries = []
try:
import pygame
pygame_dir = Path(pygame.__file__).parent

# SDL2 DLL names that pygame needs
required_dlls = [
'SDL2.dll',
'SDL2_image.dll',
'SDL2_mixer.dll',
'SDL2_ttf.dll'
]

# Find all DLLs in pygame directory
for dll_file in pygame_dir.glob('*.dll'):
binaries.append((str(dll_file), '.'))
print(f"Including pygame DLL: {dll_file.name}")

print(f"Found {len(binaries)} pygame DLLs")
return binaries

except ImportError:
print("Warning: pygame not found, no DLLs will be included")
return []

# Get pygame DLLs automatically
pygame_binaries = find_pygame_binaries()

a = Analysis(
['lemonade_arcade/main.py'],
pathex=[str(base_dir)],
binaries=pygame_binaries,
datas=[
# Include static files and templates
('lemonade_arcade/static', 'lemonade_arcade/static'),
('lemonade_arcade/templates', 'lemonade_arcade/templates'),
],
hiddenimports=[
'uvicorn.lifespan.on',
'uvicorn.lifespan.off',
'uvicorn.protocols.websockets.auto',
'uvicorn.protocols.websockets.websockets_impl',
'uvicorn.protocols.http.auto',
'uvicorn.protocols.http.h11_impl',
'uvicorn.loops.auto',
'uvicorn.loops.asyncio',
'fastapi',
'fastapi.routing',
'fastapi.staticfiles',
'fastapi.templating',
'jinja2',
'pygame',
'pygame._sdl2',
'pygame._sdl2.audio',
'pygame._sdl2.controller',
'pygame._sdl2.mixer',
'pygame._sdl2.sdl2',
'pygame._sdl2.touch',
'pygame._sdl2.video',
'httpx',
'httpx._client',
'httpx._config',
'httpx._models',
'httpx._types',
'httpx._auth',
'httpx._exceptions',
'httpcore',
'httpcore._sync',
'httpcore._async',
'h11',
'h2',
'certifi',
'charset_normalizer',
'idna',
'sniffio',
],
hookspath=[],
hooksconfig={},
runtime_hooks=['hook_pygame.py'],
excludes=['SDL3'], # Explicitly exclude SDL3 to avoid conflicts
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=None)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='LemonadeArcade',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True, # Show console window for debugging
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='img/icon.ico'
)
Loading
Loading