Lorye Go! Hong Kong Edition - News

Lorye Go! Hong Kong under development in early stage.

From Code to Bundle:
Debugging the macOS Freeze in Lorye Go!

Published on March 29, 2025 by Entz Yeung

What Happened

I developed a Pygame-based game called "Lorye Go! 香港版" and bundled it into a macOS application using PyInstaller. The game worked perfectly when running the dist/main/main executable directly in the Terminal, but when launched as a proper macOS app bundle (dist/main.app) via open or double-clicking, it consistently froze after the destination selection screen (e.g., after displaying "有結果啦!下一站是... 朗屏站!"). Despite the freeze, the game successfully uploaded stats data to databases, indicating that the core logic and network functionality were intact. The issue was specific to the .app bundle's execution environment on macOS 15.2.

What I Tried So Far

Initial Testing and Observation:

I confirmed that dist/main/main worked fine, but dist/main.app froze after the destination selection. I verified that data exported well alone the pipeline, suggesting that the freeze was not related to network operations. I ran dist/main.app/Contents/MacOS/main directly in the Terminal and found that it worked, indicating that the issue was specific to the macOS app bundle context.

Debugging Attempts:

I added DEBUG messages to the game_flow function to trace the execution flow, such as DEBUG: After game_title, DEBUG: After game_start, and DEBUG: After data_export. I ran dist/main.app/Contents/MacOS/main directly and observed that the game progressed past the destination selection, with logs showing:

DEBUG: After data_export
DEBUG: Starting turn for 1
Reset BGM to 'theme'.
DEBUG: Before player_transition
DEBUG: After player_transition for player 1
        

However, when launching dist/main.app via open, I didn’t see these debug messages in the Terminal because the standard output (stdout) was not captured (because I am running the .app file).

Capturing Debug Output:

I used macOS’s log stream command to capture system logs while running the .app bundle:

log stream --predicate 'process == "main"'
        

This revealed the root cause of the crash:

2025-03-29 12:40:12.104278+0000 0x111f1d   Default     0x0                  864    0    main: [PYI-864:ERROR] Failed to execute script 'main' due to unhandled exception: [Errno 30] Read-only file system: 'screenshots'
        

The error indicated that the game was attempting to create a screenshots directory using os.makedirs("screenshots", exist_ok=True), but failed because the current working directory (/) was read-only when launched as an .app bundle. This is originated from a function I created to let the player screen capture the game screen but it renders no error in Win's binary file. That's exactly why in my opinion it is still not time for AI to replace human yet.

How I Debugged

Added Debug Messages:

I inserted DEBUG messages in the game_flow() function to trace the execution flow, such as DEBUG: After data_export and DEBUG: Starting turn for .... I modified the custom_print() function to actually print to the console:

def custom_print(*args, is_hud_message=False):
    message = " ".join(str(arg) for arg in args)
    print(message)
        

Ran the Binary Directly:

I ran dist/main.app/Contents/MacOS/main in the Terminal to compare its behavior with launching the .app bundle via open. This helped confirm that the issue was specific to the macOS app bundle context.

Used System Logs:

Since launching via open didn’t show debug output in the Terminal, I used log stream to capture system logs, which revealed the OSError: [Errno 30] Read-only file system error.

Analyzed the Error:

The traceback pointed to the os.makedirs("screenshots", exist_ok=True) call in the game_flow() function (line 3114), indicating that the game was trying to create a directory in a read-only location (/) when launched as an .app bundle.

Key Concepts to Learn

macOS App Bundle Execution Context:

When a macOS app bundle (.app) is launched via open or double-clicking, macOS sets the current working directory to the root directory (/), which is read-only for security reasons. This differs from running the binary directly, where the working directory is typically the directory containing the binary (e.g., dist/main.app/Contents/MacOS/), which is often writable.

File System Access in macOS Applications:

macOS applications should write to designated directories (e.g., the user’s home directory ~/, ~/Documents, or the app’s sandboxed container) rather than the current working directory, as the latter may be read-only or restricted.

Debugging macOS Applications:

Standard output (stdout) is not automatically directed to the Terminal when launching an .app bundle. Use log stream or redirect output to a file to capture debug messages. Example of redirecting output to a file:

def custom_print(*args, is_hud_message=False):
    message = " ".join(str(arg) for arg in args)
    print(message)
    with open(os.path.join(os.path.expanduser("~"), "LoryeGoDebug.log"), "a") as f:
        f.write(f"{message}\n")
        

PyInstaller and Resource Handling:

PyInstaller bundles the application and its resources, but it doesn’t control the runtime environment (e.g., the current working directory). Developers must ensure that file operations are performed in safe, writable locations.

Error Handling:

So it is a good reminder for me to bemindful to always wrap file operations in try-except blocks to handle potential errors gracefully, especially when dealing with file system access in cross-platform applications.

How I Solved It in the End

To resolve the issue, I made the screenshot functionality a no-op (no operation) by commenting out the code that attempted to create the screenshots directory and save screenshots. This eliminated the file system operation that was causing the crash. Here’s how I did it:

Commented Out Directory Creation:

In the game_flow() function, I commented out the line that creates the screenshots directory:

# os.makedirs("screenshots", exist_ok=True)  # Commented out to avoid directory creation
        

This prevented the game from attempting to create a directory in the read-only root directory (/).

Commented Out Screenshot Saving Logic:

In the event handling loop of the game_flow() function, I commented out the block that saves screenshots when the S key is pressed. This ensured that no file system operations related to screenshots were attempted.

Rebuilt the App:

I rebuilt the app using PyInstaller:

pyinstaller -w --add-data "assets:assets" --add-data "sub.py:." --add-data "map.py:." --add-data "question.py:." --icon "assets/icon.icns" main.py
        

Tested the .app Bundle:

I launched the .app bundle using:

open /Users/lorentzyeung/Desktop/Lorye/v994/dist/main.app
        

The game proceeded past the destination selection without crashing, confirming that the issue was resolved.

Final Outcome

By making the screenshot functionality a no-op, I eliminated the file system operation that was failing due to the read-only root directory when the .app bundle was launched. This allowed the game to run successfully in the macOS app bundle context. If I need to reintroduce screenshot functionality in the future, I should create the directory in a writable location (e.g., ~/LoryeGoScreenshots) and handle file system errors gracefully.