PyAppExec is a cross-platform bootstrapper that prepares a Python application for end users. It locates (or installs) a suitable Python interpreter, provisions an isolated virtual environment, downloads any third-party tooling you bundle, installs Python dependencies, and finally launches your target script - all driven by a simple .ini file specification. For a friendlier setup flow, PyAppExec ships a companion installer with a graphical interface that auto-detects your project layout, copies the launcher into place, generates a tailored .ini you can fine-tune, and produces ready-to-ship launcher folders for Windows/Linux or a branded .app bundle with your icon on macOS.
Many Python applications require users to install Python, set up virtual environments, download extra outside dependencies, and install Python packages before they can run the app. PyAppExec automates those steps so you can distribute a native binary launcher alongside your Python project. Ship the PyAppExec binary, ship an .ini configuration that describes what the launcher should do, and PyAppExec takes care of the rest.
While PyInstaller, cx_Freeze and similar tools are excellent when you need a completely self-contained binary, PyAppExec deliberately lightens the shipped package, relegating the heavier dependency downloads to the end user’s machine during the first run. Unlike these “freezer” tools that bundle an entire Python runtime and all dependencies into a monolithic executable, PyAppExec keeps your Python project intact and simply orchestrates interpreter provisioning, virtual environments, and external tooling on the user’s machine. That makes updates faster (swap out your Python sources without rebuilding a frozen binary), reduces download size, and keeps the runtime transparent for power users who still want to inspect or modify the Python code.
.ini sections so Windows, macOS, and Linux can point at platform-specific scripts, download URLs, and tooling.apt, dnf, or pacman if a suitable interpreter is not found.pip install runs..ini..ini for you to customize, and produces ready-to-share artifacts (launcher folders for Windows/Linux and a branded .app on macOS).spdlog so you can tail progress or integrate with external log collectors.pyappexec.ini and selects the [<OS>:main] and [<OS>:requirements] sections that match the current platform (Linux, MacOS, Windows)..ini file.requirements.txt (or whichever file you point to) are installed inside that environment. A signature file prevents redundant installs when the requirements file has not changed..ini file, PyAppExec checks whether it is already present and satisfies the minimum version. Missing dependencies are downloaded to the distrib/ directory or installed via a custom command..
├── CMakeLists.txt # CMake build configuration for the C++ launcher
├── main.cpp # Entry point that drives the CLI/GUI bootstrapper
├── include/ # Public headers shared between the CLI and GUI layers
├── lib/ # Core launcher implementation (AppBootstrapper, utils, etc.)
├── gui/ # Qt6 widgets for the optional front-end (MainWindow, GuiRunner)
├── resources/ # GLib resource manifest plus embedded Python helper scripts
├── scripts/ # Source copies of the embedded helper scripts
├── LICENSE / README.md # Project metadata and documentation
To build the launcher you need:
pkg-config and the development headers for gio-2.0 (part of GLib) — these provide glib-compile-resources and the GIO runtime used to embed scripts. On macOS install them with brew install glib libffi zlib.boost_filesystem + boost_system pair on Linux).curl (Linux/macOS) or the Windows URLMon APIs (already part of Win32) for downloading requirement archives.Install the toolchain and development headers via your package manager before running CMake. Example commands:
bash
sudo apt update
sudo apt install build-essential cmake pkg-config libglib2.0-dev qt6-base-dev libspdlog-dev libboost-dev libboost-filesystem-dev libboost-system-dev libcurl4-openssl-dev
If
cmakereportsCould not find a package configuration file provided by "boost_filesystem", it means thelibboost-filesystem-devpackage is missing; install it alongsidelibboost-devso Boost.Process can link against the filesystem/system libraries.
bash
sudo dnf install @development-tools cmake pkgconf-pkg-config glib2-devel qt6-qtbase-devel spdlog-devel boost-devel libcurl-devel
bash
sudo pacman -S --needed base-devel cmake pkgconf glib2 qt6-base spdlog boost curl
After installing the dependencies, build with:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
bash
brew install glib libffi zlib boost qt spdlogpkg-config should expose gio-2.0 after those installs; if it does not, export PKG_CONFIG_PATH="/opt/homebrew/opt/libffi/lib/pkgconfig:/opt/homebrew/lib/pkgconfig:/opt/homebrew/share/pkgconfig:$PKG_CONFIG_PATH" (adjust the prefixes if needed) and rerun pkg-config --modversion gio-2.0 to confirm it resolves to the Homebrew install.bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallelscripts/build_macos.sh to apply the PKG_CONFIG_PATH tweak automatically and build in one step.cl.exe, the Windows SDK, and CMake integration are available.powershell
git clone https://github.com/microsoft/vcpkg.git C:\dev\vcpkg
C:\dev\vcpkg\bootstrap-vcpkg.bat
C:\dev\vcpkg\vcpkg.exe install qtbase:x64-windows spdlog:x64-windowsPATH:powershell
cmake -S . -B build `
-DCMAKE_BUILD_TYPE=Release `
-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake `
-DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build build --parallelIf CMake still complains that
Qt6 was not found, confirm thatqtbase:x64-windowswas installed and that the same vcpkg root is passed viaCMAKE_TOOLCHAIN_FILE. SettingVCPKG_ROOT=C:\dev\vcpkgglobally also works.
The build step generates resources.c from resources/resources.xml using glib-compile-resources. Ensure that tool is discoverable on your PATH.
Use -DBUILD_LAUNCHER=OFF or -DBUILD_INSTALLER=OFF to selectively build the CLI/GUI launcher or the installer UI.
After building, run the launcher from the project root:
./build/pyappexec
PyAppExec first looks for pyappexec.ini in the current directory; if it is not found, it scans each immediate subdirectory. To point at a specific file explicitly, pass --config /path/to/pyappexec.ini.
On macOS, if you distribute PyAppExec inside an .app bundle, launch flags (for example --reset-gui) only work when you either run the embedded binary directly (Your.app/Contents/MacOS/YourBinary --reset-gui) or insert --args when using Finder/open (open Your.app --args --reset-gui). Anything after the .app path without --args is treated as a document, so PyAppExec never sees the flag.
Useful flags
--config /path/to/pyappexec.ini – override the config discovery logic described above.--no-gui – force CLI mode even when the INI requests the Qt front-end.--reset-gui – clear the persisted "hide GUI" preference (handy if you previously suppressed the GUI after a successful run).--help – print a brief overview of PyAppExec and the available flags.PyAppExec ships with an optional Qt-based installer that can scaffold a launcher/INI for an existing Python project. Build it via cmake --build build --parallel --target pyappexec_installer (or leave -DBUILD_INSTALLER=ON, which is the default). The installer allows you to:
.app bundle.pyappexec.ini for Linux/Windows/macOS with the common defaults (including the “hide GUI after successful runs” flag).Packaging the installer as a macOS .app
./scripts/package_installer_app.sh to produce dist/PyAppExec_Installer.<version>.<arch>.app. The script rebuilds the installer binary, converts the iconset under resources/icons/macos/, copies Qt frameworks/plugins via macdeployqt, and performs an ad-hoc codesign so the bundle launches without Gatekeeper warnings. Set CODESIGN_IDENTITY if you need to sign with a real certificate.PyAppExec_Installer…app directly to customers. When they launch it, they can browse to their Python project, (optionally) supply an icon, and the installer will drop the PyAppExec launcher + pyappexec.ini alongside the project, ready for redistribution on Linux, Windows, or macOS.reset_pyappexec.command/reset_pyappexec.sh helper next to the INI/launcher; it removes the managed virtual environment defined in pyappexec.ini..app that embeds your Python project under Contents/Resources/app, the launcher, and pyappexec.ini (rewritten to point at the bundled sources). The installer attempts an ad-hoc codesign; set CODESIGN_IDENTITY to sign with your certificate.pyappexec binary and a tailored pyappexec.ini into the root of the Python application you plan to ship (the repo’s pyappexec.ini shows the recommended layout where every path is relative to the INI). Rename the binary to match your product if you like.distrib/ downloads, and GUI suppression preference behave as expected. Remove any temporary distrib/ artifacts you don’t plan to prebundle.Set GUI = true under the relevant [<OS>:main] section to launch the Qt6 front-end. The GUI embeds the CLI output (PowerShell on Windows, Terminal on macOS/Linux), shows a progress indicator, and surfaces blocking error dialogs if anything fails. The window title automatically displays your Python app name followed by “(via PyAppExec)” so end users can immediately see that PyAppExec is handling the bootstrap. When a run completes successfully you can check “Hide GUI after successful runs” before closing the window; PyAppExec remembers that preference (per app) and skips the GUI going forward, automatically re-enabling it if a later run fails. The Help menu also exposes “About PyAppExec” and “About Qt” dialogs for attribution. Pass --no-gui on the command line or set GUI = false to force the traditional CLI experience even when the INI enables the GUI. Use --config /path/to/pyappexec.ini to point the launcher at a specific configuration file, and --reset-gui to clear any saved GUI suppression preference.
Logging
- log_console and log_level in [<OS>:main] control console output; the GUI always captures stdout/stderr internally.
- The launcher also writes rotating logs (5 MB x 3 files) under the user's log directory: %LOCALAPPDATA%\PyAppExec\logs\pyappexec.log on Windows, ~/Library/Logs/PyAppExec/pyappexec.log on macOS, and $XDG_STATE_HOME/pyappexec/pyappexec.log or ~/.local/state/pyappexec/pyappexec.log on Linux.
- The GUI writes its combined output to a log file next to pyappexec.ini named .pyappexec_gui.log. If you bundle PyAppExec into a macOS .app, the log lives inside the bundle at Your.app/Contents/MacOS/.pyappexec_gui.log unless you relocate it. For CLI runs, redirect stdout/stderr as desired.
PyAppExec is driven entirely by pyappexec.ini. Each operating system gets its own pair of sections: [Linux:main] and [Linux:requirements], [Windows:main] and [Windows:requirements], and so on. Keep the INI alongside your Python application (like the sample pyappexec.ini) so relative paths resolve naturally; PyAppExec automatically discovers it when launched from the project root.
| Key | Required | Description |
|---|---|---|
python_download_url |
no | URL you can surface to users if you need to link to an installer. The launcher does not download Python automatically from this URL but exposes it for logging and UX hooks. |
python_min_ver |
yes | Minimum acceptable Python version (for example 3.10). |
app_id |
yes | Stable identifier (6–20 alphanumeric characters) used to scope per-app caches/virtualenvs. Keep this consistent across platforms. |
config_root |
no | Root directory for cached state/venv. Defaults per-OS to a user data path (~/Library/Application Support/PyAppExec/<app_id> on macOS, $XDG_DATA_HOME/pyappexec/<app_id> or ~/.local/share/pyappexec/<app_id> on Linux, %LOCALAPPDATA%\\PyAppExec\\<app_id> on Windows). |
python_app_dir |
no | Directory that contains your Python project. Defaults to the INI file's directory. |
exec_app_path |
yes | Entry-point script relative to python_app_dir, usually your main.py. |
exec_app_args |
no | Extra command-line arguments appended after the entry script. Quote values with spaces ("--flag value"). |
exec_env |
no | Semicolon-separated list of KEY=VALUE pairs that override environment variables for the launched app. |
requirements_file |
no | Relative path to the Python requirements file. If omitted, Python dependency installation is skipped. |
virtual_env_dir |
no | Directory to create the virtual environment in. If empty, defaults to <config_root>/venv. If relative, it is resolved against python_app_dir. |
GUI |
no | true to launch the Qt front-end; false to stay purely CLI. Users can also pass --no-gui at runtime to force CLI regardless of config. |
GUI_HIDE_AFTER_SUCCESS |
no | true to automatically set the “hide GUI after successful runs” preference once a run succeeds. |
log_console |
no | true (default) to emit logs to stdout/stderr, false to silence console output (the GUI still captures logs internally). |
log_level |
no | Minimum severity for console logs. Supports trace, debug, info, warn, error, critical, off. |
The GUI installer auto-generates a valid app_id for you; override it if you want a specific identifier shared across platforms or releases.
Example: exec_env = APP_ENV=production;LOG_LEVEL="info" injects two variables, while exec_app_args = --profile default --no-telemetry adds both flags after the entry script.
Define any number of requirement_<n> blocks (numbered sequentially from 1). Each block supports:
| Key | Description |
|---|---|
requirement_<n> |
Human-friendly name, e.g. FFmpeg. |
requirement_<n>_url |
Download URL for an installer or archive. The file is stored under distrib/. |
requirement_<n>_file_name |
Override for the downloaded filename. Defaults to the basename of the URL. |
requirement_<n>_version_check_command |
Command used to detect the installed version, such as ffmpeg -version. |
requirement_<n>_version_regex |
Regular expression with a capture group that extracts the version number. |
requirement_<n>_min_version |
Minimum acceptable version; omit to accept any detected version. |
requirement_<n>_launch_file |
Name of the executable you expect after extraction (used primarily for logging). |
requirement_<n>_capture_stderr |
When true, merges stderr into stdout during version detection. |
requirement_<n>_cmd_params |
Extra parameters appended when running a Windows installer executable. |
requirement_<n>_install_command |
Shell command to run for installing the requirement. If provided, PyAppExec skips the built-in download/extract logic and just runs this command (even when requirement_<n>_url is empty). |
requirement_<n>_append_to_path |
true to prepend the requirement’s bin directory to PATH when launching your app (inferred from the version check command or extract location). |
requirement_<n>_standalone |
true to install/extract to a standalone location instead of distrib/ (Windows zip auto-extract uses %ProgramFiles%/<name> when no install_dir is given). |
requirement_<n>_install_dir |
Explicit install/extract directory used when standalone is true. |
PyAppExec keeps per-requirement status in memory to avoid noisy logs when the version check command is run multiple times.
Archive/installer handling
- Built-in auto-extraction kicks in when both install_command and cmd_params are empty. On Windows it handles .zip via PowerShell. On macOS/Linux it will attempt to extract .tar.* via tar, .zip via unzip, and .7z/.rar via 7z/7za if those tools are on PATH; otherwise you must provide an install_command.
- Any other formats (executable installers, pkg/msi, unsupported archives) require an explicit install_command.
- When append_to_path is true, the launcher prepends the requirement’s bin directory to PATH before starting your app, whether the requirement was auto-extracted or detected via version_check_command.
The repository ships with a Linux configuration that targets the sample app in test/:
[Linux:main]
python_download_url = https://www.python.org/ftp/python/3.13.1/Python-3.13.1.tgz
python_min_ver = 3.10
python_app_dir = test
exec_app_path = src/your_package/__main__.py
requirements_file = requirements.txt
virtual_env_dir = .pyappexec-venv
GUI = true
log_console = true
log_level = info
; exec_app_args = --profile default
; exec_env = APP_ENV=production
[Linux:requirements]
requirement_1 = FFmpeg
requirement_1_url = https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
requirement_1_file_name = ffmpeg-release-amd64-static.tar.xz
requirement_1_version_check_command = ffmpeg -version
requirement_1_version_regex = ^ffmpeg version ([0-9.]+)
requirement_1_min_version = 4.4.2
requirement_1_launch_file = ffmpeg
requirement_1_capture_stderr = false
requirement_1_install_command = sudo apt-get update && sudo apt-get install -y ffmpeg
[Windows:main]
python_download_url = https://www.python.org/ftp/python/3.13.1/python-3.13.1-amd64.exe
python_min_ver = 3.10
python_app_dir = test
exec_app_path = src/your_package/__main__.py
requirements_file = requirements.txt
virtual_env_dir = .pyappexec-venv
GUI = true
log_console = true
log_level = info
[Windows:requirements]
requirement_1 = FFmpeg
requirement_1_url = https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.7z
requirement_1_file_name = ffmpeg-release-essentials.7z
requirement_1_version_check_command = ffmpeg -version
requirement_1_version_regex = ^ffmpeg version ([0-9.]+)
requirement_1_min_version = 7.0
requirement_1_capture_stderr = false
requirement_1_cmd_params = /S /quiet
On Windows, optional cmd_params can be supplied to run silent installers.
A macOS profile follows the same pattern with [MacOS:main] and [MacOS:requirements] sections; the sample pyappexec.ini shows how to point those entries at the same application while using a platform-appropriate Python installer URL and Homebrew-based FFmpeg installation command.
pyproject.toml, Poetry, Pipenv, uv, etc.PyAppExec installs dependencies by running pip install -r <requirements_file> inside the managed virtual environment. Projects that rely on pyproject.toml-only builds or external tooling can still be launched with a few extra steps:
poetry export --without-hashes --format requirements.txt > requirements.txt, pipenv lock -r > requirements.txt, uv pip compile pyproject.toml -o requirements.txt). Point requirements_file at the exported artifact and refresh it whenever dependencies change.requirements_file, set exec_app_args or wrap your entry point so the launcher runs poetry run …, pipenv run …, uv run …, etc. You can vendor the tool inside your project and install it via requirements.txt if needed.exec_app_path at a bespoke bootstrap.py that shells out to Poetry/Pipenv/uv as required before launching your real app.Regardless of approach, remember that PyAppExec always executes inside its own virtual environment. If your tool manages environments internally (for example pipenv --venv), either disable the virtual env in pyappexec.ini (point virtual_env_dir to a location you control and skip creating it) or ensure the tool is happy with the interpreter PyAppExec provisions.
Place offline installers, archives, or wheel files in the distrib/ directory. PyAppExec stores downloads here and skips re-downloading when the file already exists with the expected size (checked via HTTP Content-Length). You can pre-populate this directory before shipping your package to avoid runtime downloads.
The test/ directory contains the open-source YT Channel Downloader project as an integration example. To try the full flow:
ffmpeg is installed or let the Linux configuration install it for you../build/pyappexec from the repository root. The launcher will set up a virtual environment under test/.pyappexec-venv, install Python dependencies from test/requirements.txt, and start the PyQt application defined in test/main.py.All content from this README plus additional deep dives is mirrored under readthedocs/ so it can be published on Read the Docs. To preview the site locally:
python -m venv .docs-venv
source .docs-venv/bin/activate
pip install sphinx sphinx-rtd-theme
(cd readthedocs && sphinx-build -b html . _build/html)
Open _build/html/index.html in your browser to inspect the generated documentation.
scripts/get_python_version.py using GLib resources; edit resources/resources.xml if you need to ship additional helper scripts..pyappexec_requirements_state under the virtual environment directory to cache the requirements signature. Delete it if you need to force a reinstall.compile_commands.json in your build directory (thanks to CMAKE_EXPORT_COMPILE_COMMANDS=ON). Point Codacy/Cppcheck at that file (or copy it to the repo root) so they analyze the real translation units instead of header-only snapshots.Lightweight smoke tests validate the INI layout, Read the Docs structure, and GUI build configuration. Run them with:
python tests/run_all.py
The test scripts live under tests/ and can be extended as the project grows.
PATH. On macOS, apps launched from Finder inherit the default /usr/bin:/bin:/usr/sbin:/sbin PATH, so PyAppExec now probes /opt/homebrew/bin/python3, /usr/local/bin/python3, and /Library/Frameworks/Python.framework/Versions/Current/bin/python3 automatically; you can also export PYAPPEXEC_PYTHON=/absolute/path/to/python3 before starting the launcher (or wrap your .app with that environment) to force a specific interpreter. On Linux, check the console logs to see if PyAppExec attempted package-manager installation.glib-compile-resources missing: Install the GLib development tools package (libglib2.0-dev-bin on Debian/Ubuntu, glib2 on Arch, brew install glib on macOS).test/.pyappexec-venv) and rerun the launcher to force a clean setup.Issues and pull requests are welcome. Please include reproduction steps and platform details when reporting bugs. If you plan to contribute substantial changes, open a discussion first so we can align on direction.
This project is released under the MIT License.
PyAppExec builds on the excellent work of the open-source community:
Thank you to the maintainers and contributors of these projects for making PyAppExec possible.