Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .github/workflows/build-ubuntu-sdist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ jobs:
python3 -m build --sdist --outdir dist .
pip3 install dist/pygame_ce-*.tar.gz -vv

- name: Install PyInstaller
run: pip3 install pyinstaller

- name: Run tests
env:
SDL_VIDEODRIVER: "dummy"
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ jobs:
shell: msys2 {0}
run: python3 dev.py --ignore-dep ninja build --wheel /artifacts --lax

- name: Install PyInstaller
shell: msys2 {0}
run: pip install pyinstaller

- name: Run tests
env:
SDL_VIDEODRIVER: "dummy"
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/run-ubuntu-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ jobs:
run: |
pyenv global ${{ matrix.python }}-debug
python dev.py build --lax --coverage --ctest --sanitize undefined

- name: Install PyInstaller
run: |
pyenv global ${{ matrix.python }}-debug
pip install pyinstaller

- name: Run tests
env:
SDL_VIDEODRIVER: "dummy"
Expand Down
10 changes: 10 additions & 0 deletions test/pyinstaller_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__tags__ = []

exclude = False
try:
import PyInstaller
except ImportError:
exclude = True

if exclude:
__tags__.extend(("ignore", "subprocess_ignore"))
93 changes: 93 additions & 0 deletions test/pyinstaller_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import os
import subprocess
import sys
import tempfile
import unittest

import pygame
import pygame.examples


class PyinstallerTest(unittest.TestCase):
"""Tests that pygame-ce can be frozen into a standalone executable
using PyInstaller.

Note: PyInstaller cannot see into pygame-ce's editable-install import
hook, so this test only reflects a normal (non-editable) install of
pygame-ce, matching how CI and end users actually install the package.
"""

def test_pyinstaller_build_and_run(self):
"""A minimal pygame-ce script can be frozen with PyInstaller and
the resulting executable runs successfully (exit code 0)."""
if any(name.endswith("_editable_loader") for name in sys.modules):
self.skipTest(
"pygame-ce is installed in editable mode; PyInstaller cannot "
"see through the editable-install import hook to bundle it"
)

example_script = os.path.join(
os.path.dirname(os.path.abspath(pygame.examples.__file__)),
"headless_no_windows_needed.py",
)
self.assertTrue(
os.path.isfile(example_script),
f"expected example script not found: {example_script}",
)

with tempfile.TemporaryDirectory() as tmp_dir:
dist_path = os.path.join(tmp_dir, "dist")
build_path = os.path.join(tmp_dir, "build")
spec_path = tmp_dir

build_cmd = [
sys.executable,
"-m",
"PyInstaller",
"--onefile",
"--distpath",
dist_path,
"--workpath",
build_path,
"--specpath",
spec_path,
example_script,
]

build_result = subprocess.run(
build_cmd,
timeout=120,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
self.assertEqual(
build_result.returncode,
0,
f"PyInstaller build failed:\n{build_result.stderr.decode(errors='replace')}",
)

exe_name = "headless_no_windows_needed"
if sys.platform == "win32":
exe_name += ".exe"
exe_path = os.path.join(dist_path, exe_name)

self.assertTrue(
os.path.isfile(exe_path),
f"expected frozen executable not found: {exe_path}",
)

run_result = subprocess.run(
[exe_path],
timeout=30,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
self.assertEqual(
run_result.returncode,
0,
f"frozen executable exited nonzero:\n{run_result.stderr.decode(errors='replace')}",
)


if __name__ == "__main__":
unittest.main()
Loading