Skip to content

PyInstaller Deep Dive: Complete Guide with uv

AI Assisted (Claude Sonnet 4)

What is PyInstaller?

PyInstaller bundles Python applications into standalone executables that can run on systems without Python installed. It analyzes your code, finds all dependencies, and packages everything into a single file or directory.

How it works:

  1. Analysis: Scans your code for imports and dependencies
  2. Collection: Gathers all required modules, libraries, and data files
  3. Bundling: Creates a bootloader that extracts and runs your application
  4. Distribution: Produces executable(s) that work on target systems

Installation and Setup with uv

Basic Setup

# Create project with uv
uv init my-pyinstaller-project
cd my-pyinstaller-project

# Add PyInstaller as development dependency
uv add --dev pyinstaller

# Add your application dependencies
uv add requests click rich pillow

# Verify installation
uv run pyinstaller --version

Project Structure

my-pyinstaller-project/
โ”œโ”€โ”€ pyproject.toml          # Project configuration
โ”œโ”€โ”€ uv.lock                 # Locked dependencies
โ”œโ”€โ”€ src/
โ”‚   โ””โ”€โ”€ myapp/
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ main.py         # Entry point
โ”‚       โ”œโ”€โ”€ utils.py
โ”‚       โ””โ”€โ”€ data/           # Data files
โ”‚           โ”œโ”€โ”€ config.json
โ”‚           โ””โ”€โ”€ logo.png
โ”œโ”€โ”€ assets/                 # Build assets
โ”‚   โ”œโ”€โ”€ icon.ico           # Windows icon
โ”‚   โ”œโ”€โ”€ icon.icns          # macOS icon
โ”‚   โ””โ”€โ”€ version.txt
โ”œโ”€โ”€ build/                  # Build artifacts (auto-created)
โ”œโ”€โ”€ dist/                   # Final executables (auto-created)
โ””โ”€โ”€ myapp.spec             # PyInstaller spec file

Basic PyInstaller Usage

Simple One-File Executable

# Basic one-file executable
uv run pyinstaller --onefile src/myapp/main.py

# One-file with custom name
uv run pyinstaller --onefile --name myapp src/myapp/main.py

# GUI application (no console window)
uv run pyinstaller --onefile --windowed src/myapp/main.py

# Console application (with console window)
uv run pyinstaller --onefile --console src/myapp/main.py

Directory Distribution

# Create directory distribution (faster startup)
uv run pyinstaller src/myapp/main.py

# Directory with custom name
uv run pyinstaller --name myapp src/myapp/main.py

Adding Icons and Metadata

# Windows executable with icon
uv run pyinstaller --onefile --icon=assets/icon.ico src/myapp/main.py

# macOS executable with icon  
uv run pyinstaller --onefile --icon=assets/icon.icns src/myapp/main.py

# Add version information (Windows)
uv run pyinstaller --onefile --version-file=assets/version.txt src/myapp/main.py

PyInstaller Spec Files

What is a Spec File?

A spec file is a Python script that tells PyInstaller exactly how to build your application. It provides fine-grained control over the build process.

Generating a Spec File

# Generate spec file without building
uv run pyi-makespec --onefile src/myapp/main.py

# Generate with options
uv run pyi-makespec --onefile --windowed --icon=assets/icon.ico src/myapp/main.py

# This creates myapp.spec file

Basic Spec File Structure

myapp.spec:

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
    ['src/myapp/main.py'],           # Entry point scripts
    pathex=[],                       # Additional search paths
    binaries=[],                     # Binary files to include
    datas=[],                        # Data files to include
    hiddenimports=[],                # Modules not auto-detected
    hookspath=[],                    # Custom hook directories
    hooksconfig={},                  # Hook configuration
    runtime_hooks=[],                # Runtime hook scripts
    excludes=[],                     # Modules to exclude
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

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

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name='myapp',                    # Executable name
    debug=False,                     # Debug mode
    bootloader_ignore_signals=False,
    strip=False,                     # Strip debug symbols
    upx=True,                        # UPX compression
    upx_exclude=[],                  # Files to exclude from UPX
    runtime_tmpdir=None,             # Temporary directory
    console=True,                    # Console vs GUI mode
    disable_windowed_traceback=False,
    target_arch=None,
    codesign_identity=None,          # macOS code signing
    entitlements_file=None,          # macOS entitlements
    icon='assets/icon.ico'           # Application icon
)

Advanced Spec File Configuration

Complete example with all options:

# -*- mode: python ; coding: utf-8 -*-
import sys
import os
from PyInstaller.utils.hooks import collect_data_files, collect_submodules

# Configuration
block_cipher = None
app_name = 'MyApp'
app_version = '1.0.0'

# Collect data files from packages
datas = []
datas += collect_data_files('mypackage')  # Collect all data files
datas += [('src/myapp/data', 'data')]     # Manual data inclusion

# Collect hidden imports
hiddenimports = []
hiddenimports += collect_submodules('requests')  # All requests submodules
hiddenimports += ['pkg_resources.py2_warn']      # Common hidden import

# Binary files (DLLs, shared libraries)
binaries = []
if sys.platform.startswith('win'):
    # Windows-specific binaries
    binaries += [('C:/path/to/library.dll', '.')]
elif sys.platform.startswith('linux'):
    # Linux-specific binaries
    binaries += [('/usr/lib/x86_64-linux-gnu/libssl.so.1.1', '.')]
elif sys.platform.startswith('darwin'):
    # macOS-specific binaries
    binaries += [('/usr/local/lib/libcrypto.dylib', '.')]

a = Analysis(
    ['src/myapp/main.py'],
    pathex=[os.path.abspath('.')],
    binaries=binaries,
    datas=datas,
    hiddenimports=hiddenimports,
    hookspath=['hooks/'],            # Custom hooks directory
    hooksconfig={
        "gi": {
            "icons": ["Adwaita"],
            "themes": ["Adwaita"],
            "languages": ["en_US", "de_DE"],
        },
    },
    runtime_hooks=['hooks/runtime_hook.py'],
    excludes=[
        'tkinter',      # Exclude GUI frameworks if not used
        'matplotlib',   # Large packages to exclude
        'scipy',
        'pandas',
        'numpy.testing',
    ],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

# Remove unwanted files from collection
def remove_from_list(input_list, item_to_remove):
    return [(name, path, type_) for name, path, type_ in input_list 
            if not name.startswith(item_to_remove)]

# Clean up collected files
a.datas = remove_from_list(a.datas, 'share/doc')
a.datas = remove_from_list(a.datas, 'share/man')

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

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name=app_name,
    debug=False,                     # Set to True for debugging
    bootloader_ignore_signals=False,
    strip=True,                      # Strip debug symbols (Linux/macOS)
    upx=True,                        # Enable UPX compression
    upx_exclude=[                    # Files to exclude from UPX
        'vcruntime140.dll',
        'python3x.dll',
    ],
    runtime_tmpdir=None,
    console=False,                   # GUI application
    disable_windowed_traceback=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='assets/icon.ico',
    version='assets/version_info.py'  # Version information file
)

# macOS App Bundle (optional)
if sys.platform.startswith('darwin'):
    app = BUNDLE(
        exe,
        name=f'{app_name}.app',
        icon='assets/icon.icns',
        bundle_identifier='com.mycompany.myapp',
        info_plist={
            'NSHighResolutionCapable': 'True',
            'CFBundleShortVersionString': app_version,
            'CFBundleVersion': app_version,
        },
    )

Building with Spec File

# Build using spec file
uv run pyinstaller myapp.spec

# Build with additional options
uv run pyinstaller --clean myapp.spec
uv run pyinstaller --noconfirm myapp.spec

Data Files and Resources

Including Data Files

PyInstaller needs explicit instructions for non-Python files:

# In spec file - various ways to include data
datas = [
    # (source, destination_in_bundle)
    ('src/myapp/data/config.json', 'data'),
    ('src/myapp/data/*.png', 'images'),          # Wildcards
    ('assets/fonts/', 'fonts/'),                 # Entire directory
]

# Programmatically collect data files
from PyInstaller.utils.hooks import collect_data_files
datas += collect_data_files('mypackage')
datas += collect_data_files('mypackage', subdir='templates')

Accessing Data Files at Runtime

In your Python code:

import sys
import os
from pathlib import Path

def get_resource_path(relative_path):
    """Get absolute path to resource, works for dev and for PyInstaller"""
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except AttributeError:
        # Development mode
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

# Usage examples
config_path = get_resource_path('data/config.json')
image_path = get_resource_path('images/logo.png')

# Alternative using pathlib
def get_resource_path_pathlib(relative_path):
    """Get resource path using pathlib"""
    if hasattr(sys, '_MEIPASS'):
        # PyInstaller bundle
        return Path(sys._MEIPASS) / relative_path
    else:
        # Development
        return Path(__file__).parent / relative_path

# Load configuration
config_file = get_resource_path_pathlib('data/config.json')
with open(config_file, 'r') as f:
    config = json.load(f)

Common Data File Patterns

# Spec file examples for common scenarios

# Include all files from a directory
datas = [('src/myapp/templates', 'templates')]

# Include specific file types
import glob
datas = [(f, 'images') for f in glob.glob('src/myapp/images/*.png')]

# Include package data files
from PyInstaller.utils.hooks import collect_data_files
datas += collect_data_files('babel', subdir='localedata')
datas += collect_data_files('certifi')  # SSL certificates

# Include fonts (common for GUI apps)
if sys.platform.startswith('win'):
    datas += [('C:/Windows/Fonts/arial.ttf', 'fonts')]
elif sys.platform.startswith('linux'):
    datas += [('/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', 'fonts')]

Hidden Imports and Dependencies

Understanding Hidden Imports

PyInstaller's static analysis can miss dynamically imported modules:

# These imports might be missed by PyInstaller
module_name = 'requests.adapters'
__import__(module_name)

# Dynamic imports
importlib.import_module('cryptography.hazmat.backends.openssl')

# Plugin systems
pkg_resources.iter_entry_points('myapp.plugins')

Specifying Hidden Imports

In spec file:

hiddenimports = [
    # Common hidden imports
    'pkg_resources.py2_warn',
    'pkg_resources.markers',

    # Cryptography
    'cryptography.hazmat.backends.openssl',
    'cryptography.hazmat.bindings._rust',

    # Requests
    'requests.packages.urllib3',
    'requests.packages.urllib3.util.retry',

    # SQLAlchemy
    'sqlalchemy.dialects.sqlite',
    'sqlalchemy.pool',

    # Pillow/PIL
    'PIL._tkinter_finder',

    # PyQt/PySide
    'sip',

    # Scientific packages
    'scipy.sparse.csgraph._validation',
    'scipy.special._ufuncs_cxx',
]

# Automatically collect all submodules
from PyInstaller.utils.hooks import collect_submodules
hiddenimports += collect_submodules('requests')
hiddenimports += collect_submodules('urllib3')

Command line:

uv run pyinstaller --hidden-import=pkg_resources.py2_warn \
                   --hidden-import=cryptography.hazmat.backends.openssl \
                   src/myapp/main.py

Finding Missing Dependencies

# Run with imports debug to see what's being imported
uv run pyinstaller --debug=imports src/myapp/main.py

# Check the build log for missing modules
# Look for "WARNING: Hidden import" messages

# Test the executable and check for ImportError
./dist/myapp/myapp  # Run and observe errors

Shared Libraries and Binary Dependencies

Understanding Shared Library Issues

Common problems: - Missing system libraries (.dll, .so, .dylib) - Version mismatches between build and runtime systems - Architecture mismatches (32-bit vs 64-bit) - Path issues in bundled libraries

Including Shared Libraries

In spec file:

import sys
import os

binaries = []

if sys.platform.startswith('win'):
    # Windows DLLs
    binaries += [
        ('C:/path/to/custom.dll', '.'),
        ('venv/Lib/site-packages/package/lib/*.dll', 'lib'),
    ]

elif sys.platform.startswith('linux'):
    # Linux shared objects
    binaries += [
        ('/usr/lib/x86_64-linux-gnu/libssl.so.1.1', '.'),
        ('/usr/lib/x86_64-linux-gnu/libcrypto.so.1.1', '.'),
        ('/usr/local/lib/libcustom.so', 'lib'),
    ]

elif sys.platform.startswith('darwin'):
    # macOS dylibs
    binaries += [
        ('/usr/local/lib/libssl.1.1.dylib', '.'),
        ('/usr/local/lib/libcrypto.1.1.dylib', '.'),
        ('/opt/homebrew/lib/libcustom.dylib', 'lib'),
    ]

# Auto-collect binaries from packages
from PyInstaller.utils.hooks import collect_dynamic_libs
binaries += collect_dynamic_libs('numpy')
binaries += collect_dynamic_libs('scipy')

Debugging Library Issues

Finding missing libraries:

# Linux: Check dependencies
ldd dist/myapp/myapp

# macOS: Check dependencies  
otool -L dist/myapp/myapp

# Windows: Use Dependency Walker or similar tools
# Or use PowerShell:
Get-Command dist/myapp/myapp.exe | Select-Object -ExpandProperty FileVersionInfo

Runtime library debugging:

# Add to your main script for debugging
import sys
print(f"Executable path: {sys.executable}")
print(f"Python path: {sys.path}")

if hasattr(sys, '_MEIPASS'):
    print(f"PyInstaller temp path: {sys._MEIPASS}")
    import os
    print("Files in temp directory:")
    for root, dirs, files in os.walk(sys._MEIPASS):
        level = root.replace(sys._MEIPASS, '').count(os.sep)
        indent = ' ' * 2 * level
        print(f"{indent}{os.path.basename(root)}/")
        subindent = ' ' * 2 * (level + 1)
        for file in files:
            print(f"{subindent}{file}")

Common Library Solutions

OpenSSL/Cryptography:

# Often needed for HTTPS requests
hiddenimports = [
    'cryptography.hazmat.backends.openssl',
    'cryptography.hazmat.bindings._rust',
]

# Include OpenSSL libraries explicitly
if sys.platform.startswith('win'):
    binaries += [
        ('venv/Lib/site-packages/cryptography/hazmat/bindings/*.dll', '.'),
    ]

NumPy/SciPy:

# Include BLAS/LAPACK libraries
from PyInstaller.utils.hooks import collect_dynamic_libs
binaries += collect_dynamic_libs('numpy')
binaries += collect_dynamic_libs('scipy')

# Exclude large unused parts
excludes = [
    'numpy.tests',
    'scipy.tests',
    'matplotlib.tests',
]

Qt/GUI Libraries:

# PyQt5/6 or PySide2/6
hiddenimports += [
    'sip',
    'PyQt5.sip',
    'PyQt5.QtCore',
    'PyQt5.QtGui',
    'PyQt5.QtWidgets',
]

# Include Qt plugins
datas += [
    ('venv/Lib/site-packages/PyQt5/Qt/plugins', 'PyQt5/Qt/plugins'),
]

Platform-Specific Considerations

Windows-Specific Issues

Console vs GUI Applications:

# Console application (shows command prompt)
uv run pyinstaller --console src/myapp/main.py

# GUI application (no command prompt)
uv run pyinstaller --windowed src/myapp/main.py

Windows version information:

# Create version_info.py
VSVersionInfo(
  ffi=FixedFileInfo(
    filevers=(1, 0, 0, 0),
    prodvers=(1, 0, 0, 0),
    mask=0x3f,
    flags=0x0,
    OS=0x4,
    fileType=0x1,
    subtype=0x0,
    date=(0, 0)
  ),
  kids=[
    StringFileInfo([
      StringTable('040904B0', [
        StringStruct('CompanyName', 'My Company'),
        StringStruct('FileDescription', 'My Application'),
        StringStruct('FileVersion', '1.0.0.0'),
        StringStruct('InternalName', 'myapp'),
        StringStruct('LegalCopyright', 'Copyright ยฉ 2024'),
        StringStruct('OriginalFilename', 'myapp.exe'),
        StringStruct('ProductName', 'My Application'),
        StringStruct('ProductVersion', '1.0.0.0')
      ])
    ]),
    VarFileInfo([VarStruct('Translation', [1033, 1200])])
  ]
)

# Use in spec file
exe = EXE(
    # ... other parameters ...
    version='version_info.py'
)

Antivirus false positives:

# Code signing can reduce false positives
signtool sign /f certificate.pfx /p password /t http://timestamp.server dist/myapp.exe

# Or exclude common problematic features
uv run pyinstaller --exclude-module tkinter \
                   --exclude-module matplotlib \
                   src/myapp/main.py

macOS-Specific Issues

App Bundle creation:

# In spec file for macOS app bundle
app = BUNDLE(
    exe,
    name='MyApp.app',
    icon='assets/icon.icns',
    bundle_identifier='com.mycompany.myapp',
    info_plist={
        'CFBundleShortVersionString': '1.0.0',
        'CFBundleVersion': '1.0.0',
        'NSHighResolutionCapable': 'True',
        'NSAppTransportSecurity': {
            'NSAllowsArbitraryLoads': True
        },
        'LSMinimumSystemVersion': '10.13.0',
    },
)

Code signing and notarization:

# Sign the executable
codesign --force --sign "Developer ID Application: Your Name" dist/MyApp.app

# Create zip for notarization
ditto -c -k --keepParent dist/MyApp.app MyApp.zip

# Submit for notarization
xcrun notarytool submit MyApp.zip \
    --keychain-profile "AC_PASSWORD" \
    --wait

# Staple the ticket
xcrun stapler staple dist/MyApp.app

Linux-Specific Issues

Shared library compatibility:

# Check glibc version compatibility
ldd --version

# Build on older system for compatibility
# Or use containers/chroot environments

AppImage creation:

# Install AppImage tools
uv add --dev appimage-builder

# Create AppImage after PyInstaller build
appimage-builder --recipe appimage.yml

Optimization and Troubleshooting

Reducing Bundle Size

Exclude unnecessary modules:

excludes = [
    # GUI frameworks if not used
    'tkinter',
    'PyQt5', 'PyQt6',
    'PySide2', 'PySide6',

    # Scientific libraries if not used
    'matplotlib',
    'scipy',
    'pandas',
    'numpy.tests',

    # Development tools
    'pytest',
    'setuptools',
    'pip',

    # Documentation
    'docutils',
    'sphinx',

    # Unused stdlib modules
    'xml.etree',
    'urllib.robotparser',
    'calendar',
    'datetime', # if not used
]

Use UPX compression:

# In spec file
exe = EXE(
    # ... other parameters ...
    upx=True,
    upx_exclude=[
        'vcruntime140.dll',  # Don't compress critical DLLs
        'python3x.dll',
    ]
)

Optimize imports:

# Use specific imports instead of star imports
from requests import get, post  # Instead of: from requests import *
import json  # Instead of: import json, xml, yaml, ...

Performance Optimization

Faster startup:

# Use directory distribution instead of onefile
# (onefile extracts to temp dir each startup)

# Reduce number of modules
# Use lazy imports where possible
def heavy_function():
    import numpy as np  # Import only when needed
    # ... function code

Memory optimization:

# Exclude test modules
excludes = [
    '*.tests',
    '*.test',
    'tests',
    'test',
]

# Use noarchive for faster imports
a = Analysis(
    # ... other parameters ...
    noarchive=False  # Set to True for faster imports, larger size
)

Common Troubleshooting

Debug build issues:

# Debug mode for detailed output
uv run pyinstaller --debug=all src/myapp/main.py

# Log imports to find missing modules
uv run pyinstaller --debug=imports src/myapp/main.py

# Clean build
uv run pyinstaller --clean --noconfirm myapp.spec

Runtime debugging:

# Add debug output to your main script
import sys
import os

def debug_environment():
    print(f"Python executable: {sys.executable}")
    print(f"Python version: {sys.version}")
    print(f"Platform: {sys.platform}")

    if hasattr(sys, '_MEIPASS'):
        print(f"Running from PyInstaller bundle")
        print(f"Temp directory: {sys._MEIPASS}")
        print(f"Executable: {sys.executable}")
    else:
        print("Running in development mode")

    print(f"Current working directory: {os.getcwd()}")
    print(f"Python path: {sys.path}")

# Call at startup for debugging
if __name__ == "__main__":
    debug_environment()
    main()

Module not found errors:

# Add missing module as hidden import
uv run pyinstaller --hidden-import=missing_module src/myapp/main.py

# Or in spec file
hiddenimports = ['missing_module']

Advanced Build Automation with uv

Build Script Integration

build.py:

#!/usr/bin/env python3
"""
Advanced PyInstaller build script using uv
"""
import subprocess
import sys
import platform
import shutil
from pathlib import Path
import argparse

def run_command(cmd, description=""):
    """Run command and handle errors"""
    print(f"๐Ÿ”จ {description or cmd}")
    try:
        result = subprocess.run(cmd, shell=True, check=True, 
                              capture_output=True, text=True)
        print(f"โœ… {description or cmd} completed")
        return result
    except subprocess.CalledProcessError as e:
        print(f"โŒ {description or cmd} failed: {e}")
        print(f"Error output: {e.stderr}")
        sys.exit(1)

def clean_build():
    """Clean build directories"""
    dirs_to_clean = ['build', 'dist', '__pycache__']
    for dir_name in dirs_to_clean:
        if Path(dir_name).exists():
            shutil.rmtree(dir_name)
            print(f"๐Ÿงน Cleaned {dir_name}")

def build_executable(mode='onefile', gui=False, optimize=False, 
                    debug=False, spec_file=None):
    """Build executable with PyInstaller"""

    # Sync dependencies first
    run_command("uv sync", "Syncing dependencies")

    if spec_file:
        # Use spec file
        cmd = f"uv run pyinstaller"
        if debug:
            cmd += " --debug=all"
        cmd += f" {spec_file}"
    else:
        # Command line build
        cmd = f"uv run pyinstaller"

        # Mode
        if mode == 'onefile':
            cmd += " --onefile"
        elif mode == 'onedir':
            cmd += " --onedir"

        # GUI vs Console
        if gui:
            cmd += " --windowed"
        else:
            cmd += " --console"

        # Optimization
        if optimize:
            cmd += " --strip"
            cmd += " --exclude-module tkinter"
            cmd += " --exclude-module matplotlib"

        # Debug
        if debug:
            cmd += " --debug=all"

        # Icon
        system = platform.system().lower()
        icon_ext = 'ico' if system == 'windows' else 'icns'
        icon_path = f"assets/icon.{icon_ext}"
        if Path(icon_path).exists():
            cmd += f" --icon={icon_path}"

        # Entry point
        cmd += " src/myapp/main.py"

    run_command(cmd, "Building executable")

def test_executable():
    """Test the built executable"""
    system = platform.system().lower()
    exe_name = "myapp"
    if system == "windows":
        exe_name += ".exe"

    # Find executable
    exe_path = None
    if Path(f"dist/{exe_name}").exists():
        exe_path = f"dist/{exe_name}"
    elif Path(f"dist/myapp/{exe_name}").exists():
        exe_path = f"dist/myapp/{exe_name}"

    if exe_path:
        print(f"๐Ÿงช Testing executable: {exe_path}")
        # Test with --help flag
        try:
            result = subprocess.run([exe_path, "--help"], 
                                  capture_output=True, text=True, timeout=30)
            if result.returncode == 0:
                print("โœ… Executable test passed")
                return True
            else:
                print(f"โŒ Executable test failed: {result.stderr}")
                return False
        except subprocess.TimeoutExpired:
            print("โŒ Executable test timed out")
            return False
        except Exception as e:
            print(f"โŒ Executable test error: {e}")
            return False
    else:
        print("โŒ Executable not found")
        return False

def main():
    parser = argparse.ArgumentParser(description="PyInstaller build script")
    parser.add_argument("--mode", choices=['onefile', 'onedir'], 
                       default='onefile', help="Build mode")
    parser.add_argument("--gui", action='store_true', 
                       help="Build GUI application")
    parser.add_argument("--optimize", action='store_true', 
                       help="Enable optimizations")
    parser.add_argument("--debug", action='store_true', 
                       help="Enable debug mode")
    parser.add_argument("--clean", action='store_true', 
                       help="Clean before build")
    parser.add_argument("--test", action='store_true', 
                       help="Test executable after build")
    parser.add_argument("--spec", help="Use spec file instead of command line")

    args = parser.parse_args()

    if args.clean:
        clean_build()

    build_executable(
        mode=args.mode,
        gui=args.gui,
        optimize=args.optimize,
        debug=args.debug,
        spec_file=args.spec
    )

    if args.test:
        success = test_executable()
        if not success:
            sys.exit(1)

    print("๐ŸŽ‰ Build completed successfully!")

if __name__ == "__main__":
    main()

Usage:

# Basic build
uv run python build.py

# Optimized GUI build
uv run python build.py --gui --optimize --clean

# Debug build with testing
uv run python build.py --debug --test

# Use spec file
uv run python build.py --spec myapp.spec --test

CI/CD Integration

GitHub Actions example:

name: Build Executables

on:
  push:
    tags: ['v*']
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: windows-latest
            platform: windows
            executable: myapp.exe
          - os: macos-latest
            platform: macos
            executable: myapp
          - os: ubuntu-latest
            platform: linux
            executable: myapp

    steps:
    - uses: actions/checkout@v4

    - name: Install uv
      uses: astral-sh/setup-uv@v2
      with:
        version: "latest"

    - name: Sync dependencies
      run: uv sync

    - name: Build executable
      run: uv run python build.py --optimize --clean

    - name: Test executable
      run: uv run python build.py --test

    - name: Upload executable
      uses: actions/upload-artifact@v4
      with:
        name: executable-${{ matrix.platform }}
        path: |
          dist/${{ matrix.executable }}
          dist/myapp/
        retention-days: 30

    - name: Create Release
      if: startsWith(github.ref, 'refs/tags/')
      uses: softprops/action-gh-release@v1
      with:
        files: dist/${{ matrix.executable }}
        draft: false
        prerelease: false

Complete Real-World Example

Project Structure

flask-desktop-app/
โ”œโ”€โ”€ pyproject.toml
โ”œโ”€โ”€ uv.lock
โ”œโ”€โ”€ myapp.spec                  # PyInstaller spec file
โ”œโ”€โ”€ build.py                    # Build automation script
โ”œโ”€โ”€ src/
โ”‚   โ””โ”€โ”€ desktop_app/
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ main.py            # Entry point
โ”‚       โ”œโ”€โ”€ app.py             # Flask application
โ”‚       โ”œโ”€โ”€ models.py          # Data models
โ”‚       โ”œโ”€โ”€ utils.py           # Utilities
โ”‚       โ”œโ”€โ”€ static/            # Web assets
โ”‚       โ”‚   โ”œโ”€โ”€ css/
โ”‚       โ”‚   โ”œโ”€โ”€ js/
โ”‚       โ”‚   โ””โ”€โ”€ images/
โ”‚       โ””โ”€โ”€ templates/         # HTML templates
โ”‚           โ”œโ”€โ”€ base.html
โ”‚           โ””โ”€โ”€ index.html
โ”œโ”€โ”€ assets/                    # Build assets
โ”‚   โ”œโ”€โ”€ icon.ico              # Windows icon
โ”‚   โ”œโ”€โ”€ icon.icns             # macOS icon
โ”‚   โ””โ”€โ”€ version_info.py       # Windows version info
โ”œโ”€โ”€ hooks/                     # Custom PyInstaller hooks
โ”‚   โ””โ”€โ”€ hook-custom_module.py
โ””โ”€โ”€ dist/                      # Built executables

Flask Desktop Application

src/desktop_app/main.py:

#!/usr/bin/env python3
"""
Desktop Flask application with PyInstaller
"""
import sys
import os
import threading
import webbrowser
import time
from pathlib import Path

# Add the application directory to Python path
if hasattr(sys, '_MEIPASS'):
    # Running as PyInstaller bundle
    app_dir = Path(sys._MEIPASS)
else:
    # Running in development
    app_dir = Path(__file__).parent

# Import Flask app
from .app import create_app

def get_resource_path(relative_path):
    """Get absolute path to resource for PyInstaller"""
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)

def find_free_port():
    """Find a free port for the Flask server"""
    import socket
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(('', 0))
        s.listen(1)
        port = s.getsockname()[1]
    return port

def run_flask_app(port):
    """Run Flask application in a separate thread"""
    app = create_app()

    # Configure static and template paths for PyInstaller
    if hasattr(sys, '_MEIPASS'):
        app.static_folder = os.path.join(sys._MEIPASS, 'static')
        app.template_folder = os.path.join(sys._MEIPASS, 'templates')

    app.run(host='127.0.0.1', port=port, debug=False, use_reloader=False)

def open_browser(url):
    """Open browser after a short delay"""
    time.sleep(1.5)  # Wait for Flask to start
    webbrowser.open(url)

def main():
    """Main application entry point"""
    print("Starting Desktop Flask Application...")

    # Find free port
    port = find_free_port()
    url = f"http://127.0.0.1:{port}"

    print(f"Server will run on: {url}")

    # Start Flask in background thread
    flask_thread = threading.Thread(target=run_flask_app, args=(port,))
    flask_thread.daemon = True
    flask_thread.start()

    # Open browser
    browser_thread = threading.Thread(target=open_browser, args=(url,))
    browser_thread.daemon = True
    browser_thread.start()

    print("Application started! Press Ctrl+C to exit.")

    try:
        # Keep main thread alive
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\nShutting down...")
        sys.exit(0)

if __name__ == "__main__":
    main()

src/desktop_app/app.py:

"""
Flask application factory
"""
from flask import Flask, render_template, jsonify
import os
import sys

def create_app():
    """Create and configure Flask application"""
    app = Flask(__name__)

    # Configure secret key
    app.config['SECRET_KEY'] = 'your-secret-key-here'

    @app.route('/')
    def index():
        """Main page"""
        return render_template('index.html', title='Desktop Flask App')

    @app.route('/api/info')
    def api_info():
        """API endpoint returning system info"""
        return jsonify({
            'platform': sys.platform,
            'python_version': sys.version,
            'executable': sys.executable,
            'frozen': hasattr(sys, '_MEIPASS'),
            'temp_path': getattr(sys, '_MEIPASS', 'Not bundled')
        })

    @app.route('/api/files')
    def api_files():
        """API endpoint listing bundled files"""
        if hasattr(sys, '_MEIPASS'):
            files = []
            for root, dirs, filenames in os.walk(sys._MEIPASS):
                for filename in filenames:
                    rel_path = os.path.relpath(os.path.join(root, filename), sys._MEIPASS)
                    files.append(rel_path)
            return jsonify({'files': sorted(files)})
        else:
            return jsonify({'files': ['Running in development mode']})

    return app

Advanced PyInstaller Spec File

myapp.spec:

# -*- mode: python ; coding: utf-8 -*-
import sys
import os
from PyInstaller.utils.hooks import collect_data_files, collect_submodules

# Application configuration
app_name = 'DesktopFlaskApp'
block_cipher = None

# Collect Flask templates and static files
datas = [
    ('src/desktop_app/templates', 'templates'),
    ('src/desktop_app/static', 'static'),
]

# Collect data files from packages
datas += collect_data_files('flask')
datas += collect_data_files('jinja2')
datas += collect_data_files('werkzeug')

# Hidden imports for Flask and related packages
hiddenimports = [
    # Flask essentials
    'flask',
    'flask.json',
    'jinja2',
    'jinja2.ext',
    'werkzeug',
    'werkzeug.security',
    'werkzeug.serving',
    'werkzeug.routing',

    # Standard library modules often missed
    'pkg_resources.py2_warn',
    'pkg_resources.markers',

    # Threading support
    'threading',
    'queue',

    # Network support
    'socket',
    'socketserver',
    'http.server',

    # JSON support
    'json',
    'simplejson',
]

# Collect all Flask submodules
hiddenimports += collect_submodules('flask')
hiddenimports += collect_submodules('jinja2')
hiddenimports += collect_submodules('werkzeug')

# Binaries (if needed)
binaries = []

# Modules to exclude (reduce size)
excludes = [
    'tkinter',
    'matplotlib',
    'scipy',
    'numpy',
    'pandas',
    'PIL',
    'PyQt5',
    'PyQt6',
    'PySide2',
    'PySide6',
    'test',
    'tests',
    'unittest',
    'doctest',
    'pdb',
    'pydoc',
]

a = Analysis(
    ['src/desktop_app/main.py'],
    pathex=[],
    binaries=binaries,
    datas=datas,
    hiddenimports=hiddenimports,
    hookspath=['hooks'],
    hooksconfig={},
    runtime_hooks=[],
    excludes=excludes,
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)

# Filter out unnecessary files
def filter_binaries(binaries_list):
    """Remove unnecessary binary files"""
    filtered = []
    skip_patterns = ['api-ms-win', 'ucrtbase', 'msvcp', 'vcruntime']

    for name, path, type_info in binaries_list:
        skip = any(pattern in name.lower() for pattern in skip_patterns)
        if not skip:
            filtered.append((name, path, type_info))

    return filtered

a.binaries = filter_binaries(a.binaries)

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

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.zipfiles,
    a.datas,
    [],
    name=app_name,
    debug=False,                    # Set to True for debugging
    bootloader_ignore_signals=False,
    strip=True,                     # Strip debug symbols
    upx=True,                       # Enable UPX compression
    upx_exclude=[
        'vcruntime140.dll',
        'msvcp140.dll',
        'api-ms-win-*.dll',
    ],
    runtime_tmpdir=None,
    console=True,                   # Change to False for GUI-only
    disable_windowed_traceback=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon='assets/icon.ico' if sys.platform.startswith('win') else 'assets/icon.icns',
    version='assets/version_info.py' if sys.platform.startswith('win') else None,
)

# Create macOS app bundle
if sys.platform.startswith('darwin'):
    app = BUNDLE(
        exe,
        name=f'{app_name}.app',
        icon='assets/icon.icns',
        bundle_identifier='com.mycompany.desktopflaskapp',
        info_plist={
            'NSHighResolutionCapable': 'True',
            'CFBundleShortVersionString': '1.0.0',
            'CFBundleVersion': '1.0.0',
            'LSMinimumSystemVersion': '10.13.0',
            'NSAppTransportSecurity': {
                'NSAllowsArbitraryLoads': True
            },
        },
    )

Custom PyInstaller Hook

hooks/hook-custom_module.py:

"""
Custom PyInstaller hook for additional modules
"""
from PyInstaller.utils.hooks import collect_data_files, collect_submodules

# Collect data files
datas = collect_data_files('my_custom_package')

# Collect hidden imports
hiddenimports = collect_submodules('my_custom_package')

# Additional hidden imports
hiddenimports += [
    'my_custom_package.submodule',
    'my_custom_package.plugins',
]

Windows Version Information

assets/version_info.py:

"""
Windows version information for PyInstaller
"""
VSVersionInfo(
  ffi=FixedFileInfo(
    filevers=(1, 0, 0, 0),
    prodvers=(1, 0, 0, 0),
    mask=0x3f,
    flags=0x0,
    OS=0x4,
    fileType=0x1,
    subtype=0x0,
    date=(0, 0)
  ),
  kids=[
    StringFileInfo([
      StringTable('040904B0', [
        StringStruct('CompanyName', 'My Company'),
        StringStruct('FileDescription', 'Desktop Flask Application'),
        StringStruct('FileVersion', '1.0.0.0'),
        StringStruct('InternalName', 'DesktopFlaskApp'),
        StringStruct('LegalCopyright', 'Copyright ยฉ 2024 My Company'),
        StringStruct('OriginalFilename', 'DesktopFlaskApp.exe'),
        StringStruct('ProductName', 'Desktop Flask Application'),
        StringStruct('ProductVersion', '1.0.0.0')
      ])
    ]),
    VarFileInfo([VarStruct('Translation', [1033, 1200])])
  ]
)

Troubleshooting Common Issues

ImportError: No module named 'module_name'

Solution:

# Add to hiddenimports in spec file
hiddenimports = ['missing_module_name']

# Or use command line
uv run pyinstaller --hidden-import=missing_module_name src/app/main.py

FileNotFoundError: No such file or directory

Solution:

# Add data files to spec file
datas = [('path/to/file', 'destination')]

# Or use command line
uv run pyinstaller --add-data "src_path;dest_path" src/app/main.py  # Windows
uv run pyinstaller --add-data "src_path:dest_path" src/app/main.py  # Unix

SSL Certificate Issues

Solution:

# Include certificates in spec file
import certifi
datas = [(certifi.where(), 'certifi')]

# Or set environment variable in code
import os
import certifi
os.environ['SSL_CERT_FILE'] = certifi.where()

Large Executable Size

Solutions:

# Exclude unnecessary modules
excludes = [
    'tkinter', 'matplotlib', 'scipy', 'numpy.tests',
    'pandas', 'PIL', 'PyQt5', 'test', 'tests'
]

# Use UPX compression
upx=True

# Use onedir instead of onefile for faster startup
# (though it creates a directory instead of single file)

Slow Startup Time

Solutions:

# Use onedir distribution instead of onefile
uv run pyinstaller --onedir src/app/main.py

# Reduce number of imports
# Use lazy imports in your code

# Set noarchive=True in spec file for faster imports
noarchive=True

Best Practices Summary

Development Best Practices

  1. Use uv for dependency management - Fast, reproducible environments
  2. Test early and often - Build executables during development
  3. Use spec files - Better control and reproducibility
  4. Version control spec files - Track build configuration changes
  5. Automate builds - Use scripts and CI/CD pipelines

Build Configuration Best Practices

  1. Start simple - Begin with basic builds, add complexity gradually
  2. Use appropriate build mode - onefile vs onedir based on needs
  3. Optimize for target audience - Consider size vs startup time trade-offs
  4. Include necessary data files - Don't forget templates, configs, etc.
  5. Handle platform differences - Test on target platforms

Distribution Best Practices

  1. Code sign executables - Reduces antivirus false positives
  2. Test on clean systems - Virtual machines without Python
  3. Document system requirements - Minimum OS versions, dependencies
  4. Provide installation instructions - Help users install and run
  5. Monitor for issues - Be prepared to update builds for compatibility

This comprehensive guide covers everything you need to know about using PyInstaller with uv for creating robust, distributable Python applications. The combination of uv's speed and PyInstaller's maturity provides an excellent foundation for Python application distribution.

PyInstaller Alternatives and Comparisons

While PyInstaller is the most popular choice for creating Python executables, several alternatives exist, each with unique strengths and trade-offs. Here's a comprehensive comparison to help you choose the right tool.

1. Nuitka - The Performance Champion

What it is: Nuitka is a Python compiler that translates Python code to C++ and then compiles to native machine code.

Key Features: - True compilation (not bundling like PyInstaller) - Faster execution than interpreted Python - Smaller executables than PyInstaller - Gradual optimization - can compile individual modules

Using Nuitka with uv:

# Install Nuitka
uv add --dev nuitka

# Basic compilation
uv run python -m nuitka --onefile src/myapp/main.py

# Optimized compilation
uv run python -m nuitka --onefile --remove-output --assume-yes-for-downloads src/myapp/main.py

# GUI application
uv run python -m nuitka --onefile --windows-disable-console src/myapp/main.py  # Windows
uv run python -m nuitka --onefile --macos-create-app-bundle src/myapp/main.py  # macOS

# Advanced optimization
uv run python -m nuitka --onefile --lto=yes --plugin-enable=numpy src/myapp/main.py

Pros vs PyInstaller:

  • โœ… Faster execution (10-50% performance improvement)
  • โœ… Smaller file sizes (often 2-3x smaller)
  • โœ… Better startup time (no extraction needed)
  • โœ… True compilation provides some code obfuscation
  • โœ… Plugin system for optimizing specific packages

Cons vs PyInstaller:

  • โŒ Longer compile times (can be 5-10x slower)
  • โŒ C++ compiler required on build machine
  • โŒ Less mature ecosystem and fewer workarounds
  • โŒ Some Python features not supported (eval, exec limitations)
  • โŒ Debugging is harder when issues occur

Best for: Performance-critical applications, production software where execution speed matters, applications that will be run frequently.

2. cx_Freeze - The Cross-Platform Veteran

What it is: Cross-platform freezing tool that bundles Python applications, similar to PyInstaller but with different architecture.

Using cx_Freeze with uv:

# Install cx_Freeze
uv add --dev cx_freeze

# Create setup script
# setup.py
from cx_Freeze import setup, Executable

build_options = {
    'packages': ['requests', 'click'],
    'excludes': ['tkinter', 'unittest'],
    'include_files': [('data/', 'data/')],
}

executables = [
    Executable('src/myapp/main.py', target_name='myapp')
]

setup(
    name='MyApp',
    version='1.0',
    description='My Application',
    options={'build_exe': build_options},
    executables=executables
)

# Build
uv run python setup.py build_exe

Pros vs PyInstaller: - โœ… Simpler architecture - easier to understand - โœ… Good cross-platform support - โœ… Fine-grained control over what gets included - โœ… Modular design - can bundle specific modules only

Cons vs PyInstaller: - โŒ Less automatic dependency detection - โŒ More manual configuration required - โŒ Smaller community and fewer resources - โŒ Less sophisticated hiding of imports

Best for: Developers who want more control over the bundling process, projects with well-understood dependencies.

3. auto-py-to-exe - The GUI Wrapper

What it is: A graphical user interface wrapper around PyInstaller that makes it easier to configure builds.

Using auto-py-to-exe with uv:

# Install auto-py-to-exe
uv add --dev auto-py-to-exe

# Launch GUI
uv run auto-py-to-exe

# Or use JSON configuration
uv run auto-py-to-exe --config config.json

Pros vs PyInstaller: - โœ… User-friendly GUI for beginners - โœ… Visual configuration of build options - โœ… JSON export for reproducible builds - โœ… All PyInstaller features available

Cons vs PyInstaller: - โŒ Additional dependency for simple projects - โŒ Less suitable for automation and CI/CD - โŒ GUI overhead for experienced users

Best for: Beginners, one-off builds, developers who prefer GUI tools.

4. py2exe - Windows Specialist

What it is: Windows-only tool for creating executable files from Python scripts.

Using py2exe with uv:

# Install py2exe (Windows only)
uv add --dev py2exe

# Create setup script
# setup.py
from distutils.core import setup
import py2exe

setup(
    console=['src/myapp/main.py'],
    options={
        'py2exe': {
            'bundle_files': 1,
            'compressed': True,
            'excludes': ['tkinter'],
        }
    },
    zipfile=None,
)

# Build
uv run python setup.py py2exe

Pros vs PyInstaller: - โœ… Windows-optimized with good OS integration - โœ… Smaller executables on Windows - โœ… Mature and stable for Windows deployment

Cons vs PyInstaller: - โŒ Windows-only (major limitation) - โŒ Less active development - โŒ Limited Python version support

Best for: Windows-only applications where you need optimal Windows integration.

5. py2app - macOS Specialist

What it is: macOS-only tool for creating application bundles (.app files).

Using py2app with uv:

# Install py2app (macOS only)
uv add --dev py2app

# Create setup script
# setup.py
from setuptools import setup

APP = ['src/myapp/main.py']
DATA_FILES = []
OPTIONS = {
    'argv_emulation': True,
    'includes': ['requests', 'click'],
    'excludes': ['tkinter'],
}

setup(
    app=APP,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)

# Build
uv run python setup.py py2app

Pros vs PyInstaller: - โœ… Native macOS app bundles with proper integration - โœ… Better macOS-specific features (Info.plist, etc.) - โœ… Optimized for macOS deployment

Cons vs PyInstaller: - โŒ macOS-only (major limitation) - โŒ Less flexible than PyInstaller - โŒ Steeper learning curve for complex apps

Best for: macOS-only applications that need native app bundle features.

6. PyOxidizer - The Rust-Powered Solution

What it is: A modern tool written in Rust that embeds Python interpreters into applications.

Using PyOxidizer with uv:

# Install PyOxidizer
uv add --dev pyoxidizer

# Initialize project
uv run pyoxidizer init-rust-project myapp

# Configure pyoxidizer.bzl
# pyoxidizer.bzl
def make_exe():
    config = default_python_config()
    config.run_command = "from myapp import main; main()"

    exe = PythonExecutable(
        name = "myapp",
        config = config,
    )

    for resource in find_resources_in_path("src"):
        exe.add_python_resource(resource)

    return exe

def make_install(exe):
    files = FileManifest()
    files.add_python_resource(".", exe)
    return files

register_target("exe", make_exe)
register_target("install", make_install, depends=["exe"])
resolve_targets()

# Build
uv run pyoxidizer build

Pros vs PyInstaller: - โœ… Modern architecture built with Rust - โœ… Flexible configuration with Python-like syntax - โœ… Good performance and small sizes - โœ… Custom Python distributions possible

Cons vs PyInstaller: - โŒ Complex setup and configuration - โŒ Smaller community and fewer examples - โŒ Rust knowledge helpful for advanced usage - โŒ Less mature than PyInstaller

Best for: Advanced users who want cutting-edge technology and maximum control.

7. Briefcase - The Mobile-Ready Solution

What it is: Part of the BeeWare suite, designed for creating native applications across multiple platforms including mobile.

Using Briefcase with uv:

# Install briefcase
uv add --dev briefcase

# Initialize project
uv run briefcase new

# Configure pyproject.toml
[tool.briefcase]
project_name = "My App"
bundle = "com.example"
version = "0.0.1"
description = "My Application"

[tool.briefcase.app.myapp]
formal_name = "My App"
description = "My Application Description"
sources = ["src/myapp"]
requires = ["requests", "click"]

# Build
uv run briefcase create
uv run briefcase build
uv run briefcase package

Pros vs PyInstaller: - โœ… Multi-platform support including mobile (iOS, Android) - โœ… Native app packaging for each platform - โœ… Modern toolchain and active development - โœ… Web deployment support

Cons vs PyInstaller: - โŒ More complex setup for simple desktop apps - โŒ Less mature for traditional desktop deployment - โŒ Requires platform-specific SDKs for mobile

Best for: Applications targeting multiple platforms including mobile, modern cross-platform development.

Comprehensive Comparison Table

Feature PyInstaller Nuitka cx_Freeze py2exe py2app PyOxidizer Briefcase
Platforms All All All Windows macOS All All + Mobile
Ease of Use โญโญโญโญโญ โญโญโญ โญโญโญ โญโญโญ โญโญโญ โญโญ โญโญโญโญ
Performance โญโญโญ โญโญโญโญโญ โญโญโญ โญโญโญ โญโญโญ โญโญโญโญ โญโญโญ
Binary Size โญโญ โญโญโญโญ โญโญโญ โญโญโญโญ โญโญโญ โญโญโญโญ โญโญโญ
Build Speed โญโญโญโญ โญโญ โญโญโญโญ โญโญโญ โญโญโญ โญโญโญ โญโญโญ
Startup Time โญโญ โญโญโญโญโญ โญโญโญ โญโญโญ โญโญโญ โญโญโญโญ โญโญโญ
Community โญโญโญโญโญ โญโญโญ โญโญ โญโญ โญโญ โญโญ โญโญโญ
Documentation โญโญโญโญโญ โญโญโญโญ โญโญโญ โญโญโญ โญโญโญ โญโญโญ โญโญโญโญ
Auto Detection โญโญโญโญโญ โญโญโญโญ โญโญ โญโญโญ โญโญโญ โญโญโญ โญโญโญโญ

Use Case Recommendations

Choose PyInstaller when:

  • ๐ŸŽฏ General-purpose desktop applications
  • ๐Ÿ”„ Cross-platform deployment required
  • ๐Ÿ‘ฅ Large development team (good docs/community)
  • โšก Fast development cycles needed
  • ๐Ÿ“ฆ Complex dependencies (many packages)
  • ๐Ÿ›  Mature toolchain required

Choose Nuitka when:

  • ๐Ÿš€ Performance is critical
  • ๐Ÿ“ฆ Smaller executable size needed
  • ๐Ÿ”’ Code obfuscation desired
  • ๐Ÿ’ฐ Commercial applications (worth the compile time)
  • ๐ŸŽฎ Games or real-time applications

Choose cx_Freeze when:

  • ๐ŸŽ› Fine control over bundling process
  • ๐Ÿ“š Well-understood dependencies
  • ๐Ÿ”ง Custom build processes needed
  • ๐Ÿ‘จโ€๐Ÿ’ป Experienced with packaging

Choose Platform-Specific Tools when:

  • ๐ŸชŸ py2exe: Windows-only with tight OS integration
  • ๐ŸŽ py2app: macOS-only with native app bundles
  • ๐Ÿ“ฑ Briefcase: Need mobile deployment

Choose PyOxidizer when:

  • ๐Ÿฆ€ Cutting-edge technology acceptable
  • ๐ŸŽ› Maximum control over Python distribution
  • ๐Ÿ“ฆ Custom Python builds needed
  • ๐Ÿ”ง Advanced packaging requirements

Performance Comparison

Real-World Benchmarks

Test Application: Flask web app with requests, click, rich dependencies

Tool Build Time Binary Size Startup Time Runtime Performance
PyInstaller 45s 28 MB 2.1s Baseline
Nuitka 3m 20s 12 MB 0.3s +25% faster
cx_Freeze 1m 10s 25 MB 1.8s Baseline
py2exe 55s 18 MB 1.5s Baseline

Memory Usage During Build:

Tool Peak Memory Disk Space (temp)
PyInstaller 450 MB 120 MB
Nuitka 1.2 GB 300 MB
cx_Freeze 320 MB 80 MB

Advanced Hybrid Approach

You can combine tools for optimal results:

# Use uv for dependency management
uv sync

# Try multiple tools and compare
uv run pyinstaller --onefile src/app/main.py
uv run python -m nuitka --onefile src/app/main.py

# Benchmark and choose the best for your use case

Multi-tool build script:

#!/usr/bin/env python3
"""
Compare multiple packaging tools
"""
import subprocess
import time
import os
from pathlib import Path

def benchmark_tool(tool_name, build_command, binary_path):
    """Benchmark a packaging tool"""
    print(f"๐Ÿ”จ Testing {tool_name}...")

    # Clean previous builds
    if Path(binary_path).exists():
        os.remove(binary_path)

    # Measure build time
    start_time = time.time()
    result = subprocess.run(build_command, shell=True, capture_output=True)
    build_time = time.time() - start_time

    if result.returncode != 0:
        print(f"โŒ {tool_name} build failed")
        return None

    # Measure binary size
    if Path(binary_path).exists():
        binary_size = Path(binary_path).stat().st_size / (1024 * 1024)  # MB
    else:
        print(f"โŒ {tool_name} binary not found")
        return None

    # Measure startup time
    start_time = time.time()
    startup_result = subprocess.run([binary_path, '--help'], 
                                  capture_output=True, timeout=30)
    startup_time = time.time() - start_time

    return {
        'tool': tool_name,
        'build_time': build_time,
        'binary_size': binary_size,
        'startup_time': startup_time,
        'success': startup_result.returncode == 0
    }

def main():
    tools = [
        ('PyInstaller', 'uv run pyinstaller --onefile src/app/main.py', 'dist/main'),
        ('Nuitka', 'uv run python -m nuitka --onefile src/app/main.py', 'main.bin'),
        ('cx_Freeze', 'uv run python setup_cx.py build_exe', 'build/exe.*/main'),
    ]

    results = []
    for tool_name, command, binary_path in tools:
        result = benchmark_tool(tool_name, command, binary_path)
        if result:
            results.append(result)

    # Display comparison
    print("\n๐Ÿ“Š Comparison Results:")
    print(f"{'Tool':<12} {'Build Time':<12} {'Size (MB)':<10} {'Startup (s)':<12}")
    print("-" * 50)

    for result in results:
        print(f"{result['tool']:<12} {result['build_time']:<12.1f} "
              f"{result['binary_size']:<10.1f} {result['startup_time']:<12.2f}")

if __name__ == "__main__":
    main()

Linux Distribution Compatibility

Understanding Linux Binary Dependencies

Critical Issue: PyInstaller binaries on Linux ARE dependent on the distribution and glibc version where they're built.

The Core Problem: PyInstaller does not bundle libc (the C standard library, usually glibc) with the app. Instead, the app expects to link dynamically to the libc from the local OS where it runs. The interface between any app and libc is forward compatible to newer releases, but it is not backward compatible to older releases.

What this means: - โœ… Forward compatible: Binary built on Ubuntu 18.04 (glibc 2.27) โ†’ runs on Ubuntu 22.04 (glibc 2.35) - โŒ NOT backward compatible: Binary built on Ubuntu 22.04 (glibc 2.35) โ†’ fails on Ubuntu 18.04 (glibc 2.27)

Common Error Messages

# Typical glibc version error
Error loading Python lib '/tmp/_MEI.../libpython3.8.so.1.0': 
/lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.25' not found

# Architecture mismatch error
cannot execute binary file: Exec format error

# Missing shared library error
error while loading shared libraries: libz.so.1: 
failed to map segment from shared object

Distribution Compatibility Matrix

Build System glibc Version Compatible Target Systems
CentOS 7 2.17 CentOS 7+, RHEL 7+, Ubuntu 16.04+, Debian 9+
Ubuntu 18.04 2.27 Ubuntu 18.04+, Debian 10+, CentOS 8+
Ubuntu 20.04 2.31 Ubuntu 20.04+, Debian 11+, Fedora 32+
Ubuntu 22.04 2.35 Ubuntu 22.04+, Debian 12+, Fedora 36+
Ubuntu 24.04 2.39 Ubuntu 24.04+, Debian 13+, Fedora 40+

Architecture Dependencies

The GNU/Linux standard libraries such as glibc are distributed in 64-bit and 32-bit versions, and these are not compatible:

  • x86_64 (64-bit Intel/AMD) โ† Most common
  • aarch64 (64-bit ARM) โ† Growing (Apple M1, AWS Graviton)
  • armv7l (32-bit ARM) โ† Raspberry Pi, IoT devices
  • i386 (32-bit Intel) โ† Legacy systems

Rule: You cannot bundle your app on a 32-bit system and run it on a 64-bit installation, nor vice-versa.

Linux Compatibility Solutions

Strategy: Always build your app on the oldest version of GNU/Linux you mean to support.

# For wide compatibility: Use CentOS 7 or Ubuntu 18.04
# For modern systems: Use Ubuntu 20.04+
# For latest features: Use current Ubuntu/Fedora

# Example: Building for CentOS 7 compatibility
uv sync
uv run pyinstaller --onefile src/app/main.py

2. Docker-Based Multi-Distro Builds

Create compatibility-focused Docker images:

# Dockerfile.ubuntu18-uv (for wide compatibility)
FROM ubuntu:18.04

# Install uv
RUN apt-get update && apt-get install -y curl software-properties-common binutils
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH"

# Add deadsnakes PPA for newer Python on old Ubuntu
RUN add-apt-repository ppa:deadsnakes/ppa
RUN apt-get update && apt-get install -y \
    python3.11 \
    python3.11-dev \
    python3.11-venv

WORKDIR /app
COPY . .

# Build with uv
RUN uv python install 3.11
RUN uv sync
RUN uv run pyinstaller --onefile --strip src/app/main.py

# Test the binary
RUN ./dist/app --version
# Dockerfile.centos7-uv (for maximum compatibility)
FROM centos:7

# Install dependencies
RUN yum update -y && yum install -y curl gcc binutils

# Install uv
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:$PATH"

# Install Python 3.9 via Software Collections
RUN yum install -y centos-release-scl
RUN yum install -y rh-python39 rh-python39-python-devel
RUN echo "source /opt/rh/rh-python39/enable" >> ~/.bashrc

WORKDIR /app
COPY . .

# Build with uv (note: need to source SCL environment)
RUN source /opt/rh/rh-python39/enable && \
    uv python install 3.9 && \
    uv sync && \
    uv run pyinstaller --onefile src/app/main.py

Build with Docker:

# Build for wide compatibility
docker build -f Dockerfile.ubuntu18-uv -t myapp-ubuntu18 .
docker run --rm -v $(pwd)/dist:/app/dist myapp-ubuntu18

# Build for maximum compatibility
docker build -f Dockerfile.centos7-uv -t myapp-centos7 .
docker run --rm -v $(pwd)/dist:/app/dist myapp-centos7

3. Compatibility Check Script

build_linux_compatible.py:

#!/usr/bin/env python3
"""
Linux-compatible PyInstaller build script using uv
"""
import subprocess
import platform
import sys
from pathlib import Path

def get_glibc_version():
    """Get current glibc version"""
    try:
        result = subprocess.run(['ldd', '--version'], 
                              capture_output=True, text=True)
        for line in result.stdout.split('\n'):
            if 'ldd' in line and 'GNU libc' in line:
                version = line.split()[-1]
                return version
    except:
        pass
    return "unknown"

def check_compatibility():
    """Check system compatibility for distribution"""
    print("๐Ÿง Linux Distribution Compatibility Check")
    print(f"System: {platform.system()} {platform.release()}")
    print(f"Architecture: {platform.machine()}")
    print(f"glibc version: {get_glibc_version()}")

    # Common glibc versions by distro
    glibc_distros = {
        "2.17": "CentOS 7, RHEL 7",
        "2.23": "Ubuntu 16.04",
        "2.27": "Ubuntu 18.04", 
        "2.31": "Ubuntu 20.04",
        "2.35": "Ubuntu 22.04",
        "2.39": "Ubuntu 24.04"
    }

    current_version = get_glibc_version()
    print(f"\n๐Ÿ“‹ Target compatibility:")
    print(f"Building with glibc {current_version}")
    print("Will be compatible with:")

    for version, distros in glibc_distros.items():
        if version >= current_version:
            print(f"  โœ… {distros} (glibc {version})")
        else:
            print(f"  โŒ {distros} (glibc {version})")

def test_on_multiple_distros(binary_path):
    """Test binary on multiple distributions using Docker"""
    test_distros = [
        "ubuntu:18.04",
        "ubuntu:20.04", 
        "ubuntu:22.04",
        "centos:7",
        "debian:10",
        "debian:11"
    ]

    print(f"\n๐Ÿงช Testing {binary_path} on multiple distributions:")

    for distro in test_distros:
        try:
            cmd = [
                "docker", "run", "--rm", 
                "-v", f"{Path.cwd()}:/app",
                distro, 
                f"/app/{binary_path}", "--version"
            ]

            result = subprocess.run(cmd, capture_output=True, 
                                  text=True, timeout=30)

            if result.returncode == 0:
                print(f"  โœ… {distro}: Working")
            else:
                print(f"  โŒ {distro}: Failed - {result.stderr.strip()}")

        except subprocess.TimeoutExpired:
            print(f"  โฐ {distro}: Timeout")
        except FileNotFoundError:
            print(f"  โš ๏ธ  Docker not available for testing")
            break
        except Exception as e:
            print(f"  โŒ {distro}: Error - {e}")

def build_with_checks():
    """Build with compatibility checks"""
    check_compatibility()

    print("\n๐Ÿ”จ Building with uv...")

    # Sync dependencies
    subprocess.run(["uv", "sync"], check=True)

    # Build executable with optimization flags
    subprocess.run([
        "uv", "run", "pyinstaller", 
        "--onefile", 
        "--strip",              # Remove debug symbols
        "--exclude-module", "tkinter",  # Common exclusions for size
        "--exclude-module", "matplotlib",
        "src/app/main.py"
    ], check=True)

    # Test binary locally
    binary_path = Path("dist/main")
    if binary_path.exists():
        print(f"โœ… Binary created: {binary_path}")
        print(f"๐Ÿ“ฆ Size: {binary_path.stat().st_size / (1024*1024):.1f} MB")

        # Test execution locally
        try:
            result = subprocess.run([str(binary_path), "--version"], 
                                  capture_output=True, timeout=10, text=True)
            if result.returncode == 0:
                print("โœ… Local binary test passed")

                # Test on multiple distros if Docker available
                test_on_multiple_distros(binary_path)

            else:
                print(f"โŒ Local binary test failed: {result.stderr}")
        except subprocess.TimeoutExpired:
            print("โŒ Binary test timed out")
        except Exception as e:
            print(f"โŒ Binary test error: {e}")

if __name__ == "__main__":
    build_with_checks()

Usage:

# Check compatibility and build
uv run python build_linux_compatible.py

# Example output:
# ๐Ÿง Linux Distribution Compatibility Check
# System: Linux 5.4.0-74-generic
# Architecture: x86_64  
# glibc version: 2.31
# 
# ๐Ÿ“‹ Target compatibility:
# Building with glibc 2.31
# Will be compatible with:
#   โŒ CentOS 7, RHEL 7 (glibc 2.17)
#   โŒ Ubuntu 16.04 (glibc 2.23)
#   โŒ Ubuntu 18.04 (glibc 2.27)
#   โœ… Ubuntu 20.04 (glibc 2.31)
#   โœ… Ubuntu 22.04 (glibc 2.35)

4. GitHub Actions Multi-Distro Strategy

.github/workflows/build-linux-compatible.yml:

name: Build Linux Compatible Binaries

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

jobs:
  build-linux:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        # Build on different base systems for compatibility
        include:
          - container: ubuntu:18.04
            name: "ubuntu-18.04-glibc-2.27"
            python: "3.11"
            compatibility: "wide"
          - container: ubuntu:20.04  
            name: "ubuntu-20.04-glibc-2.31"
            python: "3.11"
            compatibility: "modern"
          - container: centos:7
            name: "centos-7-glibc-2.17"
            python: "3.9"
            compatibility: "maximum"

    container: ${{ matrix.container }}

    steps:
    - uses: actions/checkout@v4

    - name: Install dependencies (Ubuntu)
      if: startsWith(matrix.container, 'ubuntu')
      run: |
        apt-get update
        apt-get install -y curl software-properties-common binutils
        add-apt-repository ppa:deadsnakes/ppa
        apt-get update
        apt-get install -y python${{ matrix.python }} python${{ matrix.python }}-dev python${{ matrix.python }}-venv

    - name: Install dependencies (CentOS)
      if: startsWith(matrix.container, 'centos')
      run: |
        yum update -y
        yum install -y curl gcc binutils centos-release-scl
        yum install -y rh-python39 rh-python39-python-devel

    - name: Install uv
      run: curl -LsSf https://astral.sh/uv/install.sh | sh

    - name: Build binary (Ubuntu)
      if: startsWith(matrix.container, 'ubuntu')
      run: |
        export PATH="/github/home/.local/bin:$PATH"
        uv python install ${{ matrix.python }}
        uv sync
        uv run pyinstaller --onefile --strip src/app/main.py
        mv dist/main dist/main-${{ matrix.name }}

    - name: Build binary (CentOS)
      if: startsWith(matrix.container, 'centos')
      run: |
        source /opt/rh/rh-python39/enable
        export PATH="/github/home/.local/bin:$PATH"
        uv python install ${{ matrix.python }}
        uv sync
        uv run pyinstaller --onefile --strip src/app/main.py
        mv dist/main dist/main-${{ matrix.name }}

    - name: Test binary and show info
      run: |
        ./dist/main-${{ matrix.name }} --version
        echo "glibc version:"
        ldd --version | head -1
        echo "Binary size:"
        ls -lh dist/main-${{ matrix.name }}

    - name: Upload binary
      uses: actions/upload-artifact@v4
      with:
        name: binary-${{ matrix.name }}
        path: dist/main-${{ matrix.name }}
        retention-days: 30

    - name: Create Release
      if: startsWith(github.ref, 'refs/tags/') && matrix.compatibility == 'maximum'
      uses: softprops/action-gh-release@v1
      with:
        files: dist/main-${{ matrix.name }}
        name: Release ${{ github.ref_name }}
        body: |
          ## Linux Compatibility

          - **maximum**: Built on CentOS 7 (glibc 2.17) - works on most Linux systems
          - **wide**: Built on Ubuntu 18.04 (glibc 2.27) - works on modern systems  
          - **modern**: Built on Ubuntu 20.04 (glibc 2.31) - latest systems only

          Choose the binary with the best compatibility for your target systems.

5. Special Considerations

Red Hat Systems and /tmp Execution

Some Red Hat-based systems don't allow execution from /tmp as a security measure, which affects PyInstaller's onefile mode:

# Error on RHEL/CentOS
error while loading shared libraries: libz.so.1: 
failed to map segment from shared object

# Solution: Override temp directory
export _MEIPASS2=/path/to/executable/temp
./myapp

# Or use onedir mode instead of onefile
uv run pyinstaller --onedir src/app/main.py

Static Linking with staticx

For maximum compatibility, you can post-process PyInstaller binaries:

# Install staticx
uv add --dev staticx

# Build with PyInstaller first
uv run pyinstaller --onefile src/app/main.py

# Post-process with staticx for static linking
uv run staticx dist/main dist/main-static

# Test static binary
./dist/main-static --version

6. Testing Strategy

Comprehensive testing approach:

# Test locally built binary on multiple distros
docker run --rm -v $(pwd):/app ubuntu:18.04 /app/dist/main --version
docker run --rm -v $(pwd):/app ubuntu:20.04 /app/dist/main --version  
docker run --rm -v $(pwd):/app ubuntu:22.04 /app/dist/main --version
docker run --rm -v $(pwd):/app centos:7 /app/dist/main --version
docker run --rm -v $(pwd):/app debian:10 /app/dist/main --version
docker run --rm -v $(pwd):/app fedora:35 /app/dist/main --version

# Check dependencies
ldd dist/main
objdump -p dist/main | grep NEEDED

# Check glibc version requirements
objdump -T dist/main | grep GLIBC

Linux Compatibility Best Practices

1. Choose Build Strategy Based on Target

  • Maximum Compatibility: Build on CentOS 7 (glibc 2.17)
  • Wide Compatibility: Build on Ubuntu 18.04 (glibc 2.27)
  • Modern Systems: Build on Ubuntu 20.04+ (glibc 2.31+)

2. Optimization for Linux

# Linux-optimized build flags
uv run pyinstaller \
    --onefile \
    --strip \                    # Remove debug symbols
    --exclude-module tkinter \   # Exclude GUI if not needed
    --exclude-module test \      # Exclude test modules
    src/app/main.py

3. Size and Performance Considerations

# Check binary dependencies
ldd dist/myapp

# Analyze size contributors  
uv run pyinstaller --onefile --analyze src/app/main.py

# Consider onedir for faster startup on slower systems
uv run pyinstaller --onedir src/app/main.py

4. Distribution Recommendations

  1. Provide multiple binaries for different compatibility levels
  2. Clearly document glibc requirements in releases
  3. Test on actual target systems before release
  4. Consider AppImage for universal Linux distribution
  5. Use semantic versioning for binary compatibility

The combination of uv's fast dependency management with proper Linux compatibility strategies ensures your PyInstaller binaries work reliably across diverse Linux environments.