Skip to content

Instantly share code, notes, and snippets.

@srdjan-m
Created July 22, 2026 10:29
Show Gist options
  • Select an option

  • Save srdjan-m/0bef8c5a11710e6e728869ef3d29fb7e to your computer and use it in GitHub Desktop.

Select an option

Save srdjan-m/0bef8c5a11710e6e728869ef3d29fb7e to your computer and use it in GitHub Desktop.
PyInstaller spec template (written for PyInstaller 6.x)
# -*- mode: python ; coding: utf-8 -*-
# =============================================================================
# PyInstaller spec TEMPLATE (written for PyInstaller 6.x)
# -----------------------------------------------------------------------------
# A .spec file is just a Python script that PyInstaller executes. You build with:
#
# pyinstaller template.spec # normal build
# pyinstaller template.spec --noconfirm # overwrite dist/ without asking
# pyinstaller template.spec --clean # wipe the PyInstaller cache first
#
# You do NOT pass the .py entry point on the command line when using a spec —
# the entry point(s) are the `scripts` list inside Analysis() below.
#
# Two output layouts (pick ONE — see the EXE/COLLECT section near the bottom):
# * ONE-FOLDER (default here): dist/<name>/ with the .exe + a _internal/ dir.
# Faster startup (files are read in place, nothing is unpacked per run).
# * ONE-FILE: dist/<name>.exe (single file). Convenient to distribute, but
# every launch unpacks everything to a temp dir first -> slower to start.
#
# 6.x notes worth knowing:
# * Bytecode encryption (the old `cipher` / `block_cipher`) was REMOVED in 6.0.
# Don't use it; it's gone. (Old specs that still pass cipher=None are tolerated.)
# * In one-folder builds the payload now lives in a `_internal/` subfolder
# (renameable via COLLECT(contents_directory=...)).
# * `optimize=` (Python -O/-OO level) is a real Analysis argument in 6.x.
# =============================================================================
import sys
# Optional helpers — very useful for third-party packages that ship data files,
# hidden submodules, or their own DLLs/.so files. Uncomment when you need them.
# from PyInstaller.utils.hooks import (
# collect_data_files, # grab a package's bundled data files
# collect_submodules, # grab all submodules of a package (for dynamic imports)
# collect_dynamic_libs, # grab a package's compiled libraries (.dll/.so/.dylib)
# copy_metadata, # bundle a package's *.dist-info (needed by importlib.metadata)
# )
# -----------------------------------------------------------------------------
# Handy variables (not required by PyInstaller — just keeps the spec tidy).
# -----------------------------------------------------------------------------
APP_NAME = 'MyApp'
ENTRY = 'main.py'
ICON = 'assets/app.ico' # .ico on Windows, .icns on macOS, omit/None on Linux
CONSOLE = True # True = keep a console window (see stdout/stderr).
# False = windowed/GUI app, no console.
# -----------------------------------------------------------------------------
# datas: DATA files to bundle. List of (source, dest_dir_inside_bundle) tuples.
# * source may be a single file or a glob (e.g. 'assets/*.png').
# * dest is a FOLDER *inside* the bundle; '.' means the bundle root.
# * At runtime resolve these via sys._MEIPASS (see the resource_path note at bottom).
# -----------------------------------------------------------------------------
datas = [
('assets/*.png', 'assets'), # -> _internal/assets/*.png
('assets/fonts/MyFont.ttf', '.'), # -> _internal/MyFont.ttf
('assets/models/*.dat', 'models'), # -> _internal/models/*.dat
('config.default.ini', '.'), # ship a default config next to code
# From a third-party package (uncomment the helper import above):
# *collect_data_files('some_package'),
# *copy_metadata('some_package'), # if the app calls importlib.metadata
]
# -----------------------------------------------------------------------------
# binaries: BINARY files (DLLs / .so / .dylib) to bundle, same (src, dest) form.
# You usually DON'T need this — PyInstaller auto-detects most binary deps and
# package hooks handle the rest. Use it for DLLs loaded at runtime by path,
# or a private native lib the analysis can't see.
# -----------------------------------------------------------------------------
binaries = [
# ('C:/path/to/some_runtime.dll', '.'),
# *collect_dynamic_libs('some_package'),
]
# -----------------------------------------------------------------------------
# hiddenimports: modules PyInstaller's static analysis can't find on its own —
# typically imported dynamically (importlib, __import__, plugin systems, or
# referenced only in strings). If the frozen app dies with ModuleNotFoundError,
# add the missing module here.
# -----------------------------------------------------------------------------
hiddenimports = [
# 'pkg_resources.py2_warn',
# 'my_plugins.handler_a',
# *collect_submodules('my_plugins'), # pull in a whole dynamically-loaded package
]
a = Analysis(
[ENTRY], # entry-point script(s). Multiple -> multiple executables.
pathex=[], # extra dirs to add to sys.path during ANALYSIS (import search).
binaries=binaries, # see above
datas=datas, # see above
hiddenimports=hiddenimports, # see above
hookspath=[], # extra dirs holding your own hook-*.py files
hooksconfig={}, # per-hook config, e.g. {'gi': {'module-versions': {...}}}
runtime_hooks=[], # scripts executed *before* your entry point (env setup, patches)
excludes=[], # modules to EXCLUDE — shrinks the build, e.g.
# ['tkinter','matplotlib','pytest','PyQt5','test']
noarchive=False, # False: bundle .pyc into the PYZ archive (normal).
# True: leave loose .pyc files (useful for debugging).
optimize=0, # Python bytecode optimization: 0 (none), 1 (-O), 2 (-OO,
# also strips docstrings). Leave 0 unless you need it.
# module_collection_mode={ # 6.x: control how specific packages are collected
# 'my_pkg': 'py', # 'py'=source, 'pyc'=bytecode, 'pyz'=in archive,
# }, # 'pyz+py' etc. Rarely needed.
win_no_prefer_redirects=False, # Windows SxS assembly handling — leave default.
win_private_assemblies=False, # Windows SxS — leave default.
)
# PYZ = the compressed archive of your pure-Python modules.
pyz = PYZ(a.pure)
# -----------------------------------------------------------------------------
# OPTIONAL: Splash screen (shown instantly while the app unpacks/starts).
# Windows/Linux only (not macOS). Uncomment, then add `splash` and
# `splash.binaries` to the EXE()/COLLECT() call as shown in comments below.
# In your app, call pyi_splash.update_text(...) / pyi_splash.close() to control it.
# -----------------------------------------------------------------------------
# splash = Splash(
# 'assets/splash.png',
# binaries=a.binaries,
# datas=a.datas,
# text_pos=(10, 50),
# text_size=12,
# text_color='black',
# )
# =============================================================================
# OUTPUT LAYOUT — keep the ONE-FOLDER block OR the ONE-FILE block, not both.
# =============================================================================
# ------------------------- ONE-FOLDER (recommended) --------------------------
# Produces dist/<APP_NAME>/<APP_NAME>.exe plus a _internal/ payload folder.
# `exclude_binaries=True` keeps the binaries OUT of the exe so COLLECT can lay
# them beside it (that's what makes startup fast).
exe = EXE(
pyz,
a.scripts,
# splash, # <- add if using a splash screen
exclude_binaries=True, # one-folder: binaries handled by COLLECT below
name=APP_NAME,
debug=False, # True: verbose bootloader logging for diagnosing startup
bootloader_ignore_signals=False,
strip=False, # strip symbol tables (Linux/macOS); leave False on Windows
upx=True, # compress binaries with UPX if it's on PATH (smaller, slower start)
upx_exclude=[ # never UPX-compress these (compression can corrupt some DLLs)
'vcruntime140.dll',
'python3*.dll',
'qwindows.dll',
],
console=CONSOLE,
disable_windowed_traceback=False, # windowed apps: show a traceback dialog on crash
argv_emulation=False, # macOS: turn dropped-file Apple events into argv
target_arch=None, # macOS: 'x86_64', 'arm64', or 'universal2'. None = host arch.
codesign_identity=None, # macOS code-signing identity
entitlements_file=None, # macOS entitlements plist
icon=ICON,
contents_directory='_internal', # 6.x: name of the payload subfolder (default '_internal')
# version='version_info.txt', # Windows version resource (see note at bottom)
# manifest='app.manifest', # Windows: custom app manifest (e.g. request admin)
# uac_admin=False, # Windows: True -> request elevation at launch
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
# splash.binaries, # <- add if using a splash screen
strip=False,
upx=True,
upx_exclude=[],
name=APP_NAME, # -> dist/<APP_NAME>/
)
# --------------------------- ONE-FILE (alternative) --------------------------
# Comment out the ONE-FOLDER block above and use this instead for a single .exe.
# Note there's no COLLECT — a.binaries/a.datas go straight into the EXE, and
# there is no exclude_binaries flag.
#
# exe = EXE(
# pyz,
# a.scripts,
# a.binaries,
# a.datas,
# # splash,
# # splash.binaries,
# name=APP_NAME,
# debug=False,
# bootloader_ignore_signals=False,
# strip=False,
# upx=True,
# upx_exclude=[],
# runtime_tmpdir=None, # where the one-file bundle unpacks (None = OS temp)
# console=CONSOLE,
# disable_windowed_traceback=False,
# argv_emulation=False,
# target_arch=None,
# codesign_identity=None,
# entitlements_file=None,
# icon=ICON,
# # version='version_info.txt',
# )
# ----------------------------- macOS .app bundle -----------------------------
# On macOS, wrap the one-file (or COLLECT) result in a proper .app bundle.
# app = BUNDLE(
# coll, # or `exe` for a one-file macOS build
# name=f'{APP_NAME}.app',
# icon=ICON, # use a .icns here
# bundle_identifier='com.example.myapp',
# version='1.0.0',
# info_plist={ # extra Info.plist keys
# 'NSHighResolutionCapable': True,
# 'LSBackgroundOnly': False,
# },
# )
# =============================================================================
# EXTRA NOTES
# -----------------------------------------------------------------------------
# * Accessing bundled files at runtime — datas are NOT next to your .py when
# frozen. Resolve paths through sys._MEIPASS:
#
# import sys, os
# def resource_path(rel):
# base = getattr(sys, '_MEIPASS', os.path.abspath('.'))
# return os.path.join(base, rel)
# # e.g. resource_path('assets/logo.png')
#
# `sys._MEIPASS` is the bundle root at runtime (the _internal/ dir for
# one-folder, or the temp unpack dir for one-file). `getattr(sys,'frozen',False)`
# tells you whether you're running frozen vs. from source.
#
# * Windows version resource (`version='version_info.txt'`): generate a starter
# with `pyi-grab_version path\to\some.exe > version_info.txt`, then edit it.
#
# * MERGE(...) exists for building several apps that SHARE dependencies (so the
# common DLLs/modules aren't duplicated across bundles). Niche; look it up if
# you ship a suite of related executables.
#
# * Debugging a broken frozen build:
# - build with console=True and run from a terminal to see the traceback;
# - check build/<specname>/warn-<specname>.txt for missing-module warnings;
# - ModuleNotFoundError at runtime -> add it to `hiddenimports`;
# - missing data file -> add it to `datas` and load via resource_path();
# - a corrupt/broken DLL after UPX -> add it to `upx_exclude` (or set upx=False).
#
# * `--clean` (CLI) clears PyInstaller's cache; do it after changing deps or when
# a build behaves oddly. The spec itself can't set --clean; pass it on the CLI.
# =============================================================================
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment