Skip to content

Commit 53b36d9

Browse files
authored
Add a PyInstaller exe release (#1)
1 parent 16f03b4 commit 53b36d9

File tree

9 files changed

+497
-7
lines changed

9 files changed

+497
-7
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Build Lemonade Arcade Executable
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
paths:
7+
- '**'
8+
- '.github/workflows/build-arcade-exe.yml'
9+
pull_request:
10+
branches: [ main ]
11+
paths:
12+
- '**'
13+
release:
14+
types: [published]
15+
workflow_dispatch:
16+
inputs:
17+
version:
18+
description: 'Version to build (e.g., 0.1.0)'
19+
required: false
20+
default: '0.1.0'
21+
22+
jobs:
23+
build-executable:
24+
runs-on: windows-latest
25+
26+
steps:
27+
- name: Checkout repository
28+
uses: actions/checkout@v4
29+
30+
- name: Set up Python
31+
uses: actions/setup-python@v4
32+
with:
33+
python-version: '3.11'
34+
35+
- name: Cache Python dependencies
36+
uses: actions/cache@v3
37+
with:
38+
path: ~/.cache/pip
39+
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
40+
restore-keys: |
41+
${{ runner.os }}-pip-
42+
43+
- name: Install dependencies
44+
run: |
45+
python -m pip install --upgrade pip
46+
python -m pip install -e .
47+
python -m pip install pyinstaller
48+
49+
- name: Build executable with PyInstaller
50+
run: |
51+
python -m PyInstaller lemonade_arcade.spec
52+
shell: pwsh
53+
54+
- name: Verify build outputs
55+
run: |
56+
if (-not (Test-Path "dist\LemonadeArcade.exe")) {
57+
Write-Error "Executable not found"
58+
exit 1
59+
}
60+
61+
# Show file information
62+
$exe = Get-Item "dist\LemonadeArcade.exe"
63+
64+
Write-Host "Build artifact:"
65+
Write-Host " - Executable: $($exe.Name) ($([math]::Round($exe.Length / 1MB, 2)) MB)"
66+
Write-Host " - Location: $($exe.FullName)"
67+
shell: pwsh
68+
69+
- name: Upload executable artifact
70+
uses: actions/upload-artifact@v4
71+
with:
72+
name: lemonade-arcade-exe
73+
path: dist/LemonadeArcade.exe
74+
retention-days: 30
75+
76+
- name: Upload to release (if release)
77+
if: github.event_name == 'release'
78+
uses: actions/upload-release-asset@v1
79+
env:
80+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
81+
with:
82+
upload_url: ${{ github.event.release.upload_url }}
83+
asset_path: dist/LemonadeArcade.exe
84+
asset_name: LemonadeArcade-${{ github.event.release.tag_name }}.exe
85+
asset_content_type: application/octet-stream

build_exe.ps1

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Lemonade Arcade Executable Build Script
2+
# This script builds the Lemonade Arcade application using PyInstaller
3+
4+
param(
5+
[switch]$Clean,
6+
[string]$OutputDir = "dist"
7+
)
8+
9+
Write-Host "Lemonade Arcade Executable Builder" -ForegroundColor Green
10+
Write-Host "==================================" -ForegroundColor Green
11+
12+
# Get script directory
13+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
14+
Set-Location $ScriptDir
15+
16+
# Function to run commands with error handling
17+
function Invoke-Command-Safe {
18+
param([string]$Command, [string]$ErrorMessage = "Command failed")
19+
20+
Write-Host "Running: $Command" -ForegroundColor Yellow
21+
Invoke-Expression $Command
22+
23+
if ($LASTEXITCODE -ne 0) {
24+
Write-Error $ErrorMessage
25+
exit 1
26+
}
27+
}
28+
29+
# Clean previous builds if requested
30+
if ($Clean) {
31+
Write-Host "Cleaning previous builds..." -ForegroundColor Cyan
32+
if (Test-Path "build") { Remove-Item -Recurse -Force "build" }
33+
if (Test-Path "dist") { Remove-Item -Recurse -Force "dist" }
34+
if (Test-Path "__pycache__") { Remove-Item -Recurse -Force "__pycache__" }
35+
}
36+
37+
# Install dependencies
38+
Write-Host "Installing build dependencies..." -ForegroundColor Cyan
39+
Invoke-Command-Safe "python -m pip install --upgrade pip" "Failed to upgrade pip"
40+
Invoke-Command-Safe "python -m pip install pyinstaller" "Failed to install PyInstaller"
41+
42+
# Build executable
43+
Write-Host "Building executable with PyInstaller..." -ForegroundColor Cyan
44+
Invoke-Command-Safe "python -m PyInstaller lemonade_arcade.spec" "Failed to build executable"
45+
46+
# Verify executable was created
47+
$ExePath = "$OutputDir\LemonadeArcade.exe"
48+
if (-not (Test-Path $ExePath)) {
49+
Write-Error "Executable not found at $ExePath"
50+
exit 1
51+
}
52+
53+
# Show build results
54+
$ExeFile = Get-Item $ExePath
55+
Write-Host "" -ForegroundColor Green
56+
Write-Host "Build completed successfully!" -ForegroundColor Green
57+
Write-Host "Output file:" -ForegroundColor Green
58+
Write-Host " - Executable: $(Resolve-Path $ExePath)" -ForegroundColor White
59+
Write-Host " - Size: $([math]::Round($ExeFile.Length / 1MB, 2)) MB" -ForegroundColor White
60+
Write-Host "" -ForegroundColor Green
61+
Write-Host "To run the application:" -ForegroundColor Cyan
62+
Write-Host " Double-click: $ExePath" -ForegroundColor White
63+
Write-Host " Or from command line: $ExePath" -ForegroundColor White

hook_pygame.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import os
2+
import sys
3+
4+
5+
# Runtime hook for pygame DLL loading in PyInstaller
6+
def setup_pygame_environment():
7+
"""Set up environment for pygame DLL loading in PyInstaller bundle."""
8+
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
9+
# We're in a PyInstaller bundle
10+
bundle_dir = sys._MEIPASS
11+
12+
# Add the bundle directory to DLL search paths
13+
if hasattr(os, "add_dll_directory"):
14+
try:
15+
os.add_dll_directory(bundle_dir)
16+
except (OSError, FileNotFoundError):
17+
pass
18+
19+
# Set environment variables that help with SDL2 loading and prevent SDL3
20+
os.environ["SDL_VIDEODRIVER"] = "windib"
21+
os.environ["SDL_AUDIODRIVER"] = "directsound"
22+
23+
# Explicitly prefer SDL2 over SDL3
24+
os.environ["SDL_DYNAMIC_API"] = os.path.join(bundle_dir, "SDL2.dll")
25+
26+
# Add bundle to PATH for DLL resolution (put it first to prioritize our DLLs)
27+
current_path = os.environ.get("PATH", "")
28+
if bundle_dir not in current_path:
29+
os.environ["PATH"] = bundle_dir + os.pathsep + current_path
30+
31+
32+
# Run the setup immediately when this hook is imported
33+
setup_pygame_environment()

img/icon.ico

836 KB
Binary file not shown.

lemonade_arcade.spec

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# -*- mode: python ; coding: utf-8 -*-
2+
3+
import os
4+
from pathlib import Path
5+
6+
# Get the base directory - use current working directory instead of __file__
7+
base_dir = Path(os.getcwd())
8+
9+
def find_pygame_binaries():
10+
"""Find pygame DLLs automatically - works in any environment."""
11+
binaries = []
12+
try:
13+
import pygame
14+
pygame_dir = Path(pygame.__file__).parent
15+
16+
# SDL2 DLL names that pygame needs
17+
required_dlls = [
18+
'SDL2.dll',
19+
'SDL2_image.dll',
20+
'SDL2_mixer.dll',
21+
'SDL2_ttf.dll'
22+
]
23+
24+
# Find all DLLs in pygame directory
25+
for dll_file in pygame_dir.glob('*.dll'):
26+
binaries.append((str(dll_file), '.'))
27+
print(f"Including pygame DLL: {dll_file.name}")
28+
29+
print(f"Found {len(binaries)} pygame DLLs")
30+
return binaries
31+
32+
except ImportError:
33+
print("Warning: pygame not found, no DLLs will be included")
34+
return []
35+
36+
# Get pygame DLLs automatically
37+
pygame_binaries = find_pygame_binaries()
38+
39+
a = Analysis(
40+
['lemonade_arcade/main.py'],
41+
pathex=[str(base_dir)],
42+
binaries=pygame_binaries,
43+
datas=[
44+
# Include static files and templates
45+
('lemonade_arcade/static', 'lemonade_arcade/static'),
46+
('lemonade_arcade/templates', 'lemonade_arcade/templates'),
47+
],
48+
hiddenimports=[
49+
'uvicorn.lifespan.on',
50+
'uvicorn.lifespan.off',
51+
'uvicorn.protocols.websockets.auto',
52+
'uvicorn.protocols.websockets.websockets_impl',
53+
'uvicorn.protocols.http.auto',
54+
'uvicorn.protocols.http.h11_impl',
55+
'uvicorn.loops.auto',
56+
'uvicorn.loops.asyncio',
57+
'fastapi',
58+
'fastapi.routing',
59+
'fastapi.staticfiles',
60+
'fastapi.templating',
61+
'jinja2',
62+
'pygame',
63+
'pygame._sdl2',
64+
'pygame._sdl2.audio',
65+
'pygame._sdl2.controller',
66+
'pygame._sdl2.mixer',
67+
'pygame._sdl2.sdl2',
68+
'pygame._sdl2.touch',
69+
'pygame._sdl2.video',
70+
'httpx',
71+
'httpx._client',
72+
'httpx._config',
73+
'httpx._models',
74+
'httpx._types',
75+
'httpx._auth',
76+
'httpx._exceptions',
77+
'httpcore',
78+
'httpcore._sync',
79+
'httpcore._async',
80+
'h11',
81+
'h2',
82+
'certifi',
83+
'charset_normalizer',
84+
'idna',
85+
'sniffio',
86+
],
87+
hookspath=[],
88+
hooksconfig={},
89+
runtime_hooks=['hook_pygame.py'],
90+
excludes=['SDL3'], # Explicitly exclude SDL3 to avoid conflicts
91+
win_no_prefer_redirects=False,
92+
win_private_assemblies=False,
93+
cipher=None,
94+
noarchive=False,
95+
)
96+
97+
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
98+
99+
exe = EXE(
100+
pyz,
101+
a.scripts,
102+
a.binaries,
103+
a.zipfiles,
104+
a.datas,
105+
[],
106+
name='LemonadeArcade',
107+
debug=False,
108+
bootloader_ignore_signals=False,
109+
strip=False,
110+
upx=True,
111+
upx_exclude=[],
112+
runtime_tmpdir=None,
113+
console=True, # Show console window for debugging
114+
disable_windowed_traceback=False,
115+
argv_emulation=False,
116+
target_arch=None,
117+
codesign_identity=None,
118+
entitlements_file=None,
119+
icon='img/icon.ico'
120+
)

0 commit comments

Comments
 (0)