Exciting news! TCMS official website is live! Offering full-stack software services including enterprise-level custom R&D, App and mini-program development, multi-system integration, AI, blockchain, and embedded development, empowering digital-intelligent transformation across industries. Visit dev.tekin.cn to discuss cooperation!

In-depth Analysis of PyInstaller macOS Single-File Mode Dual-Process Issue

2026-03-20 18 mins read

This article analyzes the dual-process, crash, and process residue issues caused by PyInstaller's single-file mode when packaging PyQt5 M3U8 downloader on macOS. Through step-by-step troubleshooting—fixing obvious bugs, eliminating system interference, and comparative testing—it locates the compatibility bug of single-file mode. It provides reusable directory mode spec configuration...

app-2-process-issue

Preface: A Real Packaging Pitfall Review

In cross-platform Python application packaging, PyInstaller is the first choice for developers due to its ease of use and compatibility. However, when packaging a PyQt5-based M3U8 downloader for the macOS platform recently, I encountered a highly misleading dual-process running issue: the app crashed and restarted on launch, lingered in the background after exiting, and threw function errors. The troubleshooting process involved endless pitfalls in configuration, system mechanisms, and packaging modes.

This article is based on real project debugging experience, fully disassembling the root cause of the problem, troubleshooting process and ultimate solutions to help developers using the macOS+PyQt5 combination avoid the same traps. It balances practicality and technical depth, tailored for macOS+PyQt5 packaging developers and PyInstaller practitioners, with every piece of content being actionable pitfall experience and solutions.

1. Background and Problem Overview

1.1 Project Background

The packaged object is an M3U8 video downloader developed based on PyQt5, targeting the macOS platform. PyInstaller was used to package it into a .app application with the original intention of achieving lightweight single-file distribution, but a series of exceptions were triggered unexpectedly.

1.2 Core Problem Manifestations

After packaging, the application had three typical faults that seriously affected the user experience and stability:

  • Launch Abnormality: Clicking the .app icon causes the window to flash and disappear immediately, then restart automatically after a 10-30 second interval, presenting an illusion of "crash-delayed restart".
  • Exit Abnormality: After closing the main window, the application process does not terminate and remains in the background, requiring manual killing to close completely.
  • Function Abnormality: After successful launch, clicking the download button directly throws an error [Errno 30] Read-only file system: 'download', making it impossible to execute download tasks normally.

1.3 Phenomenon Reproduction and Key Evidence

Through terminal process monitoring and system log troubleshooting, the core abnormality was identified: a single launch triggers parallel dual processes, and the two processes have different launch sources and times.

Process Monitoring Command

$ ps aux | grep "M3U8 Downloader"
tekin  2930  0.0  0.0 33795560   7552   ??  S  10:49PM   0:00.83 /Volumes/.../M3U8 Downloader.app/Contents/MacOS/M3U8 Downloader
tekin  2952  0.4  0.1 33686008  13244   ??  S  10:49PM   0:00.15 /Volumes/.../M3U8 Downloader.app/Contents/MacOS/M3U8 Downloader

Key System Log Information

# First process: Launched normally by LaunchServices
2026-03-19 22:46:36.752 M3U8 Downloader[98631] ... launchedByLS=1

# Second process: Restarted spontaneously inside the app after 30 seconds, not triggered by the system
2026-03-19 22:47:06.604 M3U8 Downloader[98655] ... launchedByLS=0

2. Step-by-Step Troubleshooting: A Path to Root Cause

The troubleshooting process is divided into three stages, first solving surface faults, then gradually locking the core root cause, eliminating invalid interference items throughout the process to accurately locate the problem.

2.1 Stage 1: Fix Obvious Bugs (Symptomatic Treatment)

2.1.1 Fix Window Exit Residue Issue

Initially focusing on the "process not terminating after exit" problem, checking the PyQt5 application configuration revealed the core wrong setting:

# ❌ Incorrect configuration: Prevent app from exiting when the last window is closed
app.setQuitOnLastWindowClosed(False)

# ✅ Corrected configuration: Exit app when the window is closed
app.setQuitOnLastWindowClosed(True)

Add resource cleanup logic to the window close event to ensure safe thread exit:

def _cleanup_and_exit(self, event):
    # Stop download thread and wait for recycling
    if self.download_thread and self.download_thread.isRunning():
        self.download_thread.stop()
        self.download_thread.wait(1000)
    # Exit application instance
    QtWidgets.QApplication.instance().quit()
    event.accept()

Result: The exit abnormality was resolved, but the dual-process and crash issues still existed.

2.1.2 Fix Read-Only File System Error

Root cause of the download path error: The internal directory of the .app package has read-only permissions, and setting the default download directory inside the package triggers permission interception.

Solution: Migrate the default download directory to a user-writable directory, compatible with abnormal scenarios:

from pathlib import Path
import tempfile

def _set_default_output_dir(self):
    """Set default download directory to user's Downloads to avoid read-only permissions"""
    home_dir = Path.home()
    default_dir = home_dir / "Downloads" / "M3U8"
    try:
        default_dir.mkdir(parents=True, exist_ok=True)
        self.outputDir.setText(str(default_dir))
    except Exception as e:
        # Fallback: Use temporary directory
        self.outputDir.setText(str(Path(tempfile.gettempdir()) / "M3U8"))

Result: The download function returned to normal, but the dual-process problem still persisted.

2.2 Stage 2: Eliminate System Mechanism Interference (Debugging)

2.2.1 Disable macOS Crash Recovery Mechanism

Initially suspected that macOS's built-in crash recovery and window retention mechanisms triggered automatic restart, modify the Info.plist configuration:

<!-- Disable automatic restart and window retention after crash -->
<key>NSQuitAlwaysKeepsWindows</key>
<false/>
<key>LSPreventsAppSuspension</key>
<false/>
<key>LSMultipleInstancesProhibited</key>
<true/>
<key>NSQuitKeepWindows</key>
<false/>

Result: The configuration was invalid, and the dual-process phenomenon remained unchanged.

2.2.2 Implement Application Singleton Check

Prevent multiple instance launches through file locking to verify if dual processes are caused by external repeated launches:

import fcntl

# Add exclusive lock on launch
lock_file = open('/tmp/m3u8_downloader.lock', 'w')
try:
    fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
    print("[M3U8 Downloader] Singleton check passed")
except IOError:
    print("[M3U8 Downloader] Detected existing running instance, exiting...")
    return 0

Result: The first process started normally, but the second process still launched after 30 seconds, proving that the dual-process is triggered by the internal mechanism of the application rather than external repeated launches.

2.3 Stage 3: Comparative Testing to Lock Root Cause (Radical Cure)

2.3.1 Test Directly by Bypassing the .app Structure

Enter the dist directory and run the packaged executable file directly, skipping the .app container mechanism:

cd dist
./M3U8\ Downloader

Result: Single-process operation was completely normal with no crashes or residues, ruling out problems with the .app structure and Info.plist configuration.

2.3.2 Comparison of Default Configuration vs Custom Configuration

Package a test version using the PyInstaller minimal command without custom spec configuration:

pyinstaller -w -n "m3u8dl_test" -i assets/app.icns app.py

Result: Single-process operation was completely normal. Comparing the two spec files, the core difference was locked: packaging mode.

Configuration ItemCustom spec (with dual-process fault)Default spec (normal operation)
Packaging ModeSingle-file modeDirectory mode (COLLECT+BUNDLE)
EXE binaries ProcessingAll packaged into a single EXEExclude binary files
COLLECT StageNoneExists
BUNDLE StageNoneExists

3. Root Cause Location: Compatibility Trap of Single-File Mode

3.1 Core Differences Between the Two Packaging Modes

Faulty spec (Single-file mode)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,      # ❌ Package all dependencies into a single EXE
    a.datas,
    a.zipfiles,
    [],
    name='M3U8 Downloader',
    console=False,
    icon='assets/app.icns',
)
# Manually create .app structure without COLLECT+BUNDLE process

Normal spec (Directory mode)

exe = EXE(
    pyz,
    a.scripts,
    [],                  # ✅ Exclude binary files
    exclude_binaries=True,  # ✅ Key configuration: Enable directory mode
    name='M3U8 Downloader',
    console=False,
    icon='assets/app.icns',
)

# Collect dependency files into a fixed directory
coll = COLLECT(
    exe,
    a.binaries,
    a.datas,
    name='M3U8 Downloader',
)

# Automatically generate standard .app structure
app = BUNDLE(
    coll,
    name='M3U8 Downloader.app',
    bundle_identifier='com.m3u8.downloader',
)

3.2 Dual-Process Trigger Principle

Operation mechanism of PyInstaller single-file mode: On launch, the Bootloader first extracts all dependencies to the temporary directory /tmp/_MEIxxxxxx, then loads the Python interpreter and PyQt5 framework to execute the main script.

In the macOS+PyQt5 environment, the combination of temporary file extraction and Qt event loop initialization triggers macOS security mechanisms or Qt internal process management logic, leading to the parallelism of the Bootloader parent process and Qt child process, forming a dual-process phenomenon. In directory mode, no extraction is required, and dependency files are pre-stored in a fixed directory for direct loading and running, completely avoiding this problem.

3.3 Final Conclusion

💡 PyInstaller's single-file mode has a compatibility bug with the macOS+PyQt5 environment, which is the only root cause of the dual-process problem. This problem has nothing to do with the .app structure, Info.plist configuration, or macOS crash recovery mechanism, and is solely determined by the packaging mode.

4. Ultimate Solution: Reusable Configuration

4.1 Complete spec Configuration File

The following is a verified stable configuration that adopts the directory mode, automatically generates a standard .app, adapts to PyQt5 applications, and can be directly replaced and modified for use:

# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
    ['app.py'],
    pathex=[],
    binaries=[],
    datas=[
        ('assets/mpqr.jpg', 'assets'),
        ('assets/app.png', 'assets'),
        ('assets/app.icns', 'assets'),
        ('assets/app.ico', 'assets'),
    ],
    hiddenimports=[
        'PyQt5.QtCore','PyQt5.QtGui','PyQt5.QtWidgets',
        'PyQt5.uic','PyQt5.QtPrintSupport',
        'app_ui','aiohttp','m3u8',
        'Crypto.Cipher.AES','Crypto.Util.Padding',
        'urllib3','extractor','extractor.base',
    ],
    excludes=['tkinter','matplotlib','pandas','numpy','scipy'],
)

pyz = PYZ(a.pure)

# Core: Directory mode EXE configuration
exe = EXE(
    pyz,
    a.scripts,
    [],
    exclude_binaries=True,  # Mandatory: Exclude binary files
    name='M3U8 Downloader',
    debug=False,
    console=False,
    icon='assets/app.icns',
)

# Collect all dependencies
coll = COLLECT(
    exe,
    a.binaries,
    a.datas,
    strip=False,
    upx=False,
    name='M3U8 Downloader',
)

# Automatically package into a standard macOS application
app = BUNDLE(
    coll,
    name='M3U8 Downloader.app',
    icon='assets/app.icns',
    bundle_identifier='cn.tekin.m3u8.downloader',
)

4.2 Automated Packaging Script

Package a one-click packaging script to clean up old files, execute packaging, and remove macOS isolation attributes to simplify the deployment process:

#!/bin/bash
set -e

echo "🚀 Starting to package M3U8 Downloader for macOS..."

# Activate virtual environment (enable on demand)
if [ -d ".venv" ]; then
    source .venv/bin/activate
fi

# Clean up old build products
rm -rf build/ dist/

# Package according to the spec file
pyinstaller m3u8_downloader.spec --clean --log-level WARN

# Remove macOS isolation attributes to solve the "cannot open the application" problem
xattr -cr "dist/M3U8 Downloader.app"

echo -e "\n✅ Packaging completed! Application path: dist/M3U8 Downloader.app"

5. Technical Comparison and Best Practices

5.1 Advantages and Disadvantages Comparison: Single-File Mode vs Directory Mode

Feature DimensionSingle-file modeDirectory mode (Recommended)
macOS+PyQt Compatibility❌ Dual-process bug, unstable✅ No exceptions, stable operation
Launch SpeedSlow (temporary file extraction required)Fast (direct local file loading)
Process ManagementMulti-process, difficult to controlSingle-process, clear logic
Debugging DifficultyHigh (files scattered in temporary directories)Low (files concentrated in fixed directories)
Distribution SizeSmall (compressed packaging)Slightly larger (uncompressed)

5.2 Golden Rules for macOS PyQt Application Packaging

  1. Prioritize directory mode: Configure exclude_binaries=True with the COLLECT+BUNDLE process, abandon single-file mode.
  2. Prohibit manual .app construction: Let PyInstaller automatically generate the standard application structure to avoid permission and configuration incompatibility.
  3. Avoid read-only areas for paths: All read and write operations point to user directories and temporary directories, refusing to read and write inside the .app package.
  4. Test minimally first, then customize: When encountering packaging exceptions, first test with the default command to eliminate interference from custom configurations.

5.3 Efficient Debugging Tool Commands

# Real-time monitoring of process count
watch -n 1 'ps aux | grep MyApp | grep -v grep'

# Real-time viewing of application system logs
log stream --predicate 'process == "M3U8 Downloader"' --level debug

# View historical logs from the last 5 minutes
log show --predicate 'process == "M3U8 Downloader"' --last 5m

6. References and Conclusion

References

  1. PyInstaller Official Documentation: https://pyinstaller.org/en/stable/
  2. Apple Official Bundle Programming Guide: https://developer.apple.com/documentation/bundleresources/placing-content-in-a-bundle
  3. Practical Project with This Article: https://github.com/tekintian/m3u8-downloader

Conclusion: cognitive-upgrade" target="_blank">Cognitive Upgrade from Pitfalls

This PyInstaller packaging pitfall confirms a technical common sense: the seemingly convenient single-file mode often hides compatibility pitfalls in specific platform and framework combinations. Compared with pursuing "lightweight distribution", the stability and debuggability of applications are the core demands in the production environment.

For PyQt/PySide applications on the macOS platform, the directory mode (COLLECT+BUNDLE) is a proven optimal solution. When encountering complex technical problems, returning to the minimal configuration and conducting comparative tests can often quickly strip away interference and lock the root cause, which is also a general thinking for programmers in troubleshooting.

The complete practical source code and packaging configuration for the problems described in this article can be referred to in the supporting project: https://github.com/tekintian/m3u8-downloader, where you can directly download and reuse the complete packaging script and spec configuration. If your team encounters technical bottlenecks in professional software custom development, AI application custom development, Python cross-platform application implementation and other scenarios, and needs customized solutions, difficult bug troubleshooting or project implementation support, welcome to visit the official website https://ai.tekin.cn for cooperation consultation; you can also follow the official account Technology and Cognition to continuously obtain more hardcore dry goods and pitfall guides for PyInstaller practice, macOS development and AI application implementation.

Hope this practical review can help developers packaging macOS Python applications avoid detours and efficiently solve dual-process, crash and other difficult problems.

Image NewsLetter
Icon primary
Newsletter

Subscribe our newsletter

Please enter your email address below and click the subscribe button. By doing so, you agree to our Terms and Conditions.

Your experience on this site will be improved by allowing cookies Cookie Policy