Code of the Day
AdvancedPublishing

Cross-platform concerns

Windows, macOS, and Linux differ in path separators, filesystem case sensitivity, sound format support, and executable permissions — know these before shipping.

Game DevAdvanced6 min read
By the end of this lesson you will be able to:
  • Use pathlib.Path instead of string concatenation to avoid path separator bugs
  • Explain why macOS hides case-sensitivity bugs that Linux exposes
  • Know that .ogg is the safest sound format and why .mp3 can cause problems
  • Set executable permission bits on Linux binaries

A game that works perfectly on your development machine may break silently on a player's machine if they are on a different operating system. Most cross-platform bugs fall into four categories.

Path separators and pathlib

Windows uses backslashes (\) as path separators. macOS and Linux use forward slashes (/). Hardcoding paths as strings is the fastest route to a platform-specific bug:

# Breaks on Windows:
image = pygame.image.load("assets/sprites/player.png")

# Also breaks (now on Linux/macOS):
image = pygame.image.load("assets\\sprites\\player.png")

The fix is pathlib.Path, which uses the correct separator for the current OS automatically:

from pathlib import Path

ASSETS = Path(__file__).parent / "assets"

image = pygame.image.load(ASSETS / "sprites" / "player.png")
font  = pygame.font.Font(ASSETS / "fonts" / "monogram.ttf", 24)

Path objects convert to strings automatically when passed to pygame's loaders. Use / as the path join operator regardless of platform.

Case sensitivity

macOS's default filesystem (HFS+) is case-insensitive: Player.png and player.png refer to the same file. Linux's ext4 is case-sensitive: they are different files, and loading the wrong one raises FileNotFoundError.

A common workflow bug: you name your sprite Player.png but load it as player.png. It works on your Mac. It fails on every Linux machine.

The fix: adopt a consistent naming convention (all lowercase with hyphens: player-idle.png) and enforce it. Run your game on Linux — or in a Linux Docker container — before release.

# Quick check: list all asset filenames containing uppercase letters
find assets/ -name "*[A-Z]*"

Sound format compatibility

.mp3 has historically had patent issues on Linux. While the patent situation has largely expired, some minimal Linux builds still lack the codec. Use .ogg (Vorbis) for sound effects and music — it is open, patent-free, and pygame has first-class support on all platforms.

# Preferred:
pygame.mixer.Sound("assets/sfx/jump.ogg")
pygame.mixer.music.load("assets/music/theme.ogg")

# Avoid if cross-platform support matters:
# pygame.mixer.Sound("assets/sfx/jump.mp3")

Most audio editors (Audacity, LAME) export .ogg directly. For batch conversion, ffmpeg -i input.mp3 output.ogg is fast and free.

Executable permissions on Linux

A PyInstaller binary on Linux requires the executable bit to be set. If you ZIP and unzip the build on Linux, the permissions are usually preserved. If you transfer the file via certain methods (NTFS drives, some cloud storage), the permission bit may be stripped.

Players running your game see Permission denied when they try to execute it. The fix is a one-liner:

chmod +x MyGame
./MyGame

Document this in your game's README. Alternatively, provide a small launcher shell script:

#!/bin/sh
chmod +x "$(dirname "$0")/MyGame"
"$(dirname "$0")/MyGame"

macOS has an additional wrinkle: Gatekeeper blocks executables from unidentified developers. Players must right-click → Open the first time, or you can instruct them to run xattr -cr MyGame.app in Terminal. Code signing with an Apple Developer certificate eliminates this for public releases.

Where to go next

Next: lab — publish game — fixing hardcoded paths, running PyInstaller, smoke-testing the build, and writing an itch.io page.

Finished reading? Mark it complete to track your progress.

On this page