Building an Installer¶
Whittl's Build dialog turns your project into a standalone shippable executable. No Python, no Qt install, no pip — users just run it.
Opening the Build dialog¶
Click Download (F3) in the preview panel bar (bottom-right), or use the menu: Run → Build Executable.
The Build dialog opens with auto-detected defaults for your project. For most projects, you just click Build and wait.
What gets built¶
Windows¶
Whittl produces two outputs:
- Standalone
.exe— a single-file binary (via PyInstaller one-file mode). Users double-click to run. No installation, no Start Menu entry, no uninstaller. Good for casual distribution. - Setup installer — an Inno Setup
.exethat installs the app to%LOCALAPPDATA%\Programs\<YourApp>\, creates Start Menu shortcuts, registers file associations, and includes an uninstaller. Good for polished distribution.
Both are generated in the build output folder. Pick the one you want to ship.
Linux¶
Whittl produces:
- AppImage — a portable single-file binary that works on any modern Linux distro. Users
chmod +xand run. - Optional
.tar.gzportable package if you uncheck AppImage — a folder with the executable and dependencies.
Android (experimental)¶
With Target: Mobile (Flet) set, the Build dialog offers APK Export. Covered in Mobile Builds.
Icon selection¶
The Custom Icon row in the Build dialog has three buttons: Generate, Browse, Clear.
Auto-detection¶
Before you touch anything, Whittl looks for an existing icon. It scans the project folder in this order: Art/, art/, assets/, asset/, images/, img/, resources/, res/, media/. Any .ico file found is picked up — if one is named icon.ico, it wins. The dialog shows icon.ico (auto-detected) in brand-green when this happens.
If no .ico is found, the icon label shows None (default) and PyInstaller's generic placeholder ships in the binary. Fine for quick tests, but unpolished for anything you're shipping to users.
Browse¶
Click Browse to pick a .ico file manually. The file picker starts in your project's detected assets folder. Only .ico files are selectable — PNGs aren't accepted here because Windows .exe metadata wants multi-resolution .ico. If you have a PNG, either convert it externally (any paint tool exports .ico) or use Generate (covered below — produces both PNG and .ico).
Clear¶
Drop whatever icon is selected — auto-detected or manual — and fall back to the default. Occasionally useful when testing a build without icon branding.
Generate — AI Icon Generation¶
Click Generate to open the AI Icon Generator sub-dialog. Describe what you want, the AI produces an icon, you pick the one you like and click Use as Icon — it saves to your project's assets folder (both PNG and .ico) and populates the Build dialog's icon field.
Requires an image-generation backend configured in Edit → Preferences → Image Generation:
- OpenRouter (default, recommended) — routes to
google/gemini-2.5-flash-image(a.k.a. Nano Banana). Roughly $0.04 per 1024×1024 image, ~$0.01 per 512×512. Uses your existing OpenRouter API key. - Gemini direct — same underlying model, but through Google's Gemini API. Requires billing enabled on your Google AI Studio account (Gemini's free tier excludes image generation in 2026).
If neither is configured, the Generate button shows a warning pointing you at Preferences.
Writing a good prompt¶
The dialog's description field gives you the AI free-form text. The hints in the dialog say to mention:
- Style —
flat design,3D,minimalist,pixel art,hand-drawn,neon glow,isometric - Main subject / object — what the icon represents
- Colors — specific palette or mood (
blue and white,warm earth tones,monochrome navy)
Examples of prompts that work well:
| Prompt | What comes out |
|---|---|
calculator icon, flat design, blue and white, modern |
Clean calculator silhouette, blue primary color, sans-serif feel |
music player icon, minimalist, black vinyl record with red accent |
Circular vinyl shape, small red playback highlight |
weather app icon, 3D, friendly sun with clouds, warm yellows and soft blue |
Slightly stylized sun with rounded clouds, warm palette |
todo list icon, pixel art, green checkmark on paper, retro 16-bit |
Blocky pixel icon matching the retro aesthetic |
Prompts that work less well:
- Very long sentences — the model loses focus. One descriptive line beats a paragraph.
- Text on the icon — image models struggle with legible text. Say "icon" not "icon that says CALC."
- Multiple subjects — "calculator AND music player" produces confused hybrids. One icon, one subject.
Remove Background¶
The Remove Background checkbox (on by default) runs a post-process to make the icon background transparent. Uses Pillow's flood-fill from the image edges — finds the corner color, spreads it until it hits the icon foreground. Works well for icons with clean, distinct backgrounds; can eat into icon foregrounds if the AI produced a busy or gradient background.
The checkbox auto-disables if Pillow isn't installed (pip install Pillow fixes it).
Iteration¶
Generated icons are random within the prompt — click Generate Icon again to get a different attempt. Each call costs the per-image rate above. Most users generate 3-5 options before finding one they like.
What gets saved¶
When you click Use as Icon, Whittl saves:
<assets_folder>/<app_name>_icon.png— the raw transparent PNG (1024×1024)<assets_folder>/<app_name>_icon.ico— auto-converted multi-res.ico(16 / 32 / 48 / 256)
The Build dialog's icon field updates to <filename>.ico (generated) in brand-green. Subsequent builds use it automatically.
Generate once, reuse forever
Your generated icon lands in the project's assets/ folder as real files. Check them into git, copy them to another project, tweak them in a paint tool — they're yours. You don't have to regenerate on every build.
Windows vs Linux icon format¶
- Windows — needs
.icofor both the.exeitself and the installer. Generate produces both.pngand.ico; Browse accepts only.ico. - Linux AppImage — needs
.png. Generate's saved PNG works directly. If you went through Browse with a.ico, Whittl converts it during the AppImage build.
Ship a real icon
The PyInstaller default (a generic placeholder) reads as unpolished. Even a simple geometric shape or branded letter takes 10 minutes with the AI generator and dramatically improves how the app reads in Start Menu, taskbar, and Alt-Tab. RedLight is another option for quick icon polishing.
Windows installer options¶
When you tick Create Windows installer (.exe setup) in the Build dialog, an expanded panel appears with installer-specific options. All of these are optional — leave them blank and you still get a working installer.
Publisher / Studio¶
A single line of text shown in:
- The Start Menu folder (
Start Menu → <Publisher> → <App>) - Add / Remove Programs under the Publisher column
- The installer's "Published by" line
Use your studio name, your own name, or whatever you'd want your users to see. No URL, just a display name.
Version¶
Defaults to 1.0.0. Shown in:
- The installer window title (
<App> 1.2.0 Setup) - Add / Remove Programs under the Version column
- The uninstaller
Free-form — 1.0.0, 2.1, 0.9-beta, 2026.04.22 all work. Keep it consistent across releases so users can tell one version from another in Add / Remove Programs.
EULA (End User License Agreement)¶
Optional. Click Browse... next to the EULA File field and pick a .txt or .rtf file.
When set:
- The installer shows your EULA as a scrollable page early in the install flow
- Users must click I accept the agreement before they can continue
- The Next button is disabled until acceptance
Clear the field with the Clear button if you change your mind before building.
EULA formatting
Plain .txt is simplest and always readable. .rtf lets you add basic formatting (bold headings, paragraph spacing) which reads more professionally in the installer window. If you wrote your EULA in Word or Google Docs, export as RTF before pointing the picker at it.
Include with installer (extra files)¶
The Include with installer section lets you attach any additional files to the installer. Click + Add File to pick them. They're bundled into the installer and copied to the install directory alongside the main executable.
Common uses:
- User Guide PDF —
UserGuide.pdf,Manual.pdf, or similar — lands next to the.exein the install folder. Users find it if they ever browse there; linkable from your app's Help menu if you want. - README.txt — a "first-run notes" document, changelog highlights, or quick start
- Sample data —
.csv,.json,.sqlitefiles your app references or loads as starter data - Icon packs or asset folders your app reads at runtime (if they weren't bundled into PyInstaller)
- Config templates — example
config.toml,settings.ini, etc. users can edit
Each file appears in a list; use Clear to remove them all and start over. The file list only applies to the Windows installer output — the standalone .exe and Linux AppImage builds don't read this list.
Where extra files land at install time
All attached files copy into the same folder as the installed .exe — %LOCALAPPDATA%\Programs\<YourApp>\ by default (per-user install, no UAC). They're uninstalled when the user uninstalls the app.
Advanced Options¶
Click the ▶ Advanced Options disclosure in the Build dialog to reveal these. They're hidden by default because most projects build fine with the defaults — only touch these when you know why.
Windowed (no console)¶
Default: on for GUI targets (PySide6, Flet, CustomTkinter); off for General Python.
Controls whether the .exe opens a console window alongside the app:
- On (GUI apps) — no console.
print()output is silently discarded at runtime. Users see only your window. - Off (CLI apps) — console opens when the
.exeruns.print()andinput()work normally. Required for General Python / CLI tools.
Turn OFF for a GUI app if you're debugging and want to see print() output in a console alongside the window.
Single file executable¶
Default: on for Windows, disabled on Linux.
- On — PyInstaller produces one
.execontaining everything. Slower first launch (unpacks to temp), but easier to ship. - Off — PyInstaller produces a folder with the
.exe+ dozens of supporting DLLs. Faster launch, harder to distribute.
Linux note: This option is forced OFF on Linux because AppImages need directory mode for Qt plugin compatibility. Not something you can override from the dialog.
UPX compression (30-50% smaller)¶
Default: on.
UPX compresses the final executable. A typical PySide6 app drops from ~120 MB to ~60 MB. The app decompresses itself into memory at launch — small slowdown on cold start, nothing noticeable after.
The status chip next to the checkbox shows:
- "Installed" (brand tan) — UPX is on your system, build uses it
- "Will download" (muted) — Whittl downloads UPX during the build (one-time, cached)
Turn OFF if you hit antivirus false positives — UPX-compressed executables trigger more AV heuristics than uncompressed ones. (See Antivirus deletes the built .exe.)
Debug mode¶
Default: off.
Adds PyInstaller's debug flag. The built binary logs bootstrap details to stderr on startup — which module it's importing, which plugin path it's using, which DLL it's loading.
Useful exactly once: when the built binary crashes on launch and you can't figure out why. Turn on, rebuild, run from a terminal, read the output. Then turn off.
Not useful for day-to-day builds — slows launch, leaks implementation detail.
Require administrator privileges¶
Default: off.
Adds a UAC (User Account Control) manifest to the .exe. When enabled:
- The
.exeicon gets the Windows shield overlay - Double-clicking prompts for admin elevation before launch
- Every time — there's no "remember this" flow at the
.exelevel
Breaks per-user install
Whittl's default Inno Setup template installs to %LOCALAPPDATA%\Programs\<App>\ for a true per-user, no-UAC install. Turning this on defeats that — the app itself requires elevation every time it runs, regardless of where it's installed.
Only turn on if your app genuinely needs admin rights (writes to C:\Program Files, modifies HKLM registry, installs drivers, etc.). For normal apps, leave off.
Custom Flags¶
Free-form text field passed through to PyInstaller. Use when you need a flag the dialog doesn't expose.
Common useful flags:
| Flag | What it does |
|---|---|
--add-data "SRC;DST" (Windows) or --add-data "SRC:DST" (Linux) |
Bundle extra files/folders into the .exe. Example: --add-data "templates;templates" |
--hidden-import MODULE |
Force-include a module PyInstaller missed. Example: --hidden-import pkg_resources.py2_warn |
--collect-all PACKAGE |
Include EVERYTHING from a package — data files, binaries, submodules. Heavy hammer for stubborn packages like scipy, cv2, sklearn |
--collect-data PACKAGE |
Just the data files (not code) from a package. Useful for packages with bundled models, fonts, locale files |
--exclude-module MODULE |
Explicitly drop a module PyInstaller pulled in. Example: --exclude-module tkinter if your app doesn't use it |
--log-level LEVEL |
PyInstaller's own verbosity: DEBUG, INFO, WARN, ERROR. Placeholder default |
--clean |
Clear PyInstaller's cache before this build. Use when a previous build's cache is causing weird issues |
--noconfirm |
Skip the "overwrite output?" prompt (Whittl already runs non-interactively, so this is mostly a no-op) |
--splash IMAGE.png |
Show a splash screen during onefile unpacking. Only works with Single-file mode. Example: --splash splash.png |
--version-file VERSION.txt |
Windows .exe metadata (CompanyName, FileDescription, etc.) embedded in the binary properties |
Combine with spaces: --clean --hidden-import pkg_resources.py2_warn --log-level=WARN
Path separator for --add-data
Windows wants ; between SRC and DST: --add-data "assets/logo.png;assets". Linux wants : instead: --add-data "assets/logo.png:assets". PyInstaller uses the platform separator — easy to forget when cross-building.
See the PyInstaller CLI reference for the full list. Most projects never need any of this; reach for it only when a specific dependency misbehaves.
Build process (what's happening under the hood)¶
- Dependency detection. Whittl scans all imports in the project and adds them to PyInstaller's spec.
- Static analysis. Ensures all files are included, checks for common packaging pitfalls.
- PyInstaller run. Bundles Python, your code, and all dependencies into a single binary.
- Optional: Inno Setup (Windows only). If "Full setup installer" is selected, wraps the
.exein an installer. - Output. Build folder opens in your file manager.
Typical time: 1–3 minutes depending on project size and how many dependencies are pulled in. Large projects with many packages (numpy, opencv, PIL, sounddevice, etc.) take longer because PyInstaller has to scan each dependency tree.
Common build issues¶
"No module named X" in the built binary¶
PyInstaller's auto-detection missed a package. Two fixes:
- Add an explicit import at the top of
main.py:import <module>. PyInstaller picks it up. - Use Custom Flags in the Build dialog, expand Advanced Options, and add
--hidden-import <module>to the Custom Flags field. For stubborn packages with data files and submodules, use--collect-all <module>instead.
Common offenders:
- Anything dynamically imported with
importlib - Optional dependencies loaded at runtime (Pillow plugins, scipy submodules)
- Some game engines (pygame has hidden plugin imports for audio)
Output .exe is massive (100MB+)¶
PyInstaller bundles everything. Big dependencies have big footprints:
- PySide6 alone is ~60MB (Qt is huge)
- numpy, scipy, PIL add 10-20MB each
- opencv-python adds ~30MB
This is normal. A Whittl PySide6 app typically ends up 80-120MB. A simple CustomTkinter app is 15-25MB. Smaller targets (CLI tools, lightweight apps) are 10-30MB.
To shrink: remove dependencies you don't actually use. If you imported numpy to test one thing and then removed the usage but not the import, it's still shipped.
Antivirus deletes the built .exe¶
PyInstaller-built executables are a common false positive across Windows antivirus vendors. They're not malicious — the packager signature pattern just matches some suspicious heuristics.
Mitigations:
- Add your output folder to your AV's allowlist. Tells your own AV to ignore it.
- Submit false positive reports. Most AV vendors fix within 24-48 hours once they see the report.
- Code-sign the executable. Removes the trust issue for all AV vendors simultaneously. Requires a code signing certificate (~$200-500/yr, or Azure Trusted Signing at $10/mo).
This is a known limitation of unsigned PyInstaller output. Other Python app frameworks (Briefcase, Nuitka) have the same issue to varying degrees.
Build fails with "Could not create temp directory"¶
Whittl's build pipeline uses %TEMP% for intermediate files. If your drive is low on space or %TEMP% is restricted:
- Free up 1-2 GB on
C:\ - Or set
TEMPandTMPenvironment variables to a different drive with more space
Build fails mid-way with no obvious error¶
Open the terminal panel and look for the PyInstaller output — the real error is usually there. Common causes:
- Syntax error in your source code (PyInstaller bails if Python can't parse a file)
- A dependency that doesn't support PyInstaller packaging (rare but happens with some native extensions)
- Missing dynamic library (e.g., a DLL that one of your pip packages expected to find)
Export as ZIP (source code)¶
Not every project needs to ship as a binary. For open-source projects, homework / portfolio / shareable code samples, or just to hand off the source to someone else, the Build dialog has a ZIP Archive (source code) option.
How it works¶
- Open the Build dialog (Download (F3))
- At the top, switch Build Type from Executable to ZIP Archive (source code)
- The dialog simplifies — no icon picker, no installer options, just output folder and app name
- Click Export ZIP
Whittl produces <app_name>.zip containing:
- All
.pyfiles in the project assets/— images, sounds, fontsdata/— unless you explicitly exclude it (contains runtime state, which usually doesn't belong in a source export)README.txtandproject.json- Subfolders (
ui/,core/, etc.)
What's excluded from the ZIP:
archives/— rollback snapshots, not source code__pycache__/— regenerated on run.venv/— recreated fromrequirements.txt- Build output folders
When to use ZIP export¶
- Sharing source with a collaborator who wants to inspect or modify
- Archiving a project before a major refactor
- Portfolio / GitHub upload — unzip,
git init,git add, commit - Distributing to Python-familiar users who'd rather
python main.pythan download a 100 MB installer - Just a backup of a specific project outside
~/.whittl/projects/
ZIP export vs Templates¶
- Templates (
.whittl-template) — distributable format meant to be imported by another Whittl install. Reusable starting points. See Templates. - ZIP export — plain archive of
.pyfiles anyone can unzip and read. No Whittl dependency for the recipient.
Same data, different packaging philosophies.
Distributing the built app¶
Direct download¶
Host the .exe or AppImage on any file host (Dropbox, Google Drive, GitHub Releases, your own server, R2, S3). Users download and run.
LemonSqueezy / Gumroad / Itch.io¶
For paid apps, these platforms handle checkout + delivery. Upload the built file, they hand users a download link after purchase.
Auto-update distribution¶
If you want your users to get updates automatically without re-downloading, you'll need an update mechanism in your own app. Whittl itself uses the pattern in core/update_checker.py — R2 bucket, version.txt, in-app updater. You can borrow the approach for your own apps if they need the same thing.
What stays private¶
The built binary includes:
- Your Python source code (inside the PyInstaller archive — someone with effort can extract it)
- All your assets (icons, images, data files)
- Any API keys you hardcoded (don't do this — use environment variables or a config file)
The built binary does not include:
- Your Whittl license, your API keys from Whittl, your settings
- Any other projects in your Whittl folder
- Anything in
~/.whittl/beyond the specific project you're building
Treat your built binary like compiled code — it's easier to reverse than you might think (Python bytecode is close to source), so don't bundle anything you don't want shipped.
What's next¶
- Project Folder Structure — what files get included in builds
- Sharing a Project as a Template — if you want to share the source rather than a binary
- Performance Tuning — for when the built app is slow