Release history

0.10.0

2026/07/25

New image output format. format = "image" builds no installer: the run-in-place image tree is archived into dist/ as <name>-<version>-<target>.zip (Windows targets) or .tar.gz (Linux/macOS targets), under a single top-level <name>-<version>/ directory — and unlike the other formats it is valid on every platform. Launchers follow the target OS (the MSVC .exe on Windows, the relocatable POSIX shell wrappers on Linux/macOS); a new image-only target key no-launcher = true skips them so the archive contains just the installed tree.

0.9.0

2026/07/13

uv projects now export PEP 751 pylock.toml instead of requirements.txt. Per-package index pins in uv.lock (e.g. a PyTorch CUDA index) survive the export — pip fetches the recorded URLs directly. The runtime’s bundled pip is upgraded in place when it is too old to understand pylock files.

MSI builds no longer fail on deep install trees (WiX MAX_PATH limit). wix build now receives the image as an extended-length (\\?\) bind path, working around WiX’s cabinet builder crashing with a broken-pipe IOException when a source file’s absolute path exceeds 260 characters (e.g. PyTorch’s deeply nested dist-info license files).

pipenv extras no longer drop production dependencies. Extras were passed as repeated --categories flags, which replaced the default packages category and shipped an app missing its production dependencies; one combined --categories packages,<extras...> flag is emitted now.

The Windows launcher ignores Ctrl+C/Ctrl+Break. Python alone decides how to shut down (finally blocks, atexit hooks, and buffered output are preserved) and its real exit code propagates, as with CPython’s py.exe launcher.

.pyc files use hash-based invalidation. Timestamp-based .pyc went stale after MSI packaging (CAB timestamps have 2-second granularity), causing recompiles on every cold start; --invalidation-mode checked-hash makes validity independent of timestamps.

perMachine MSI shortcuts register their keypath under HKLM. It was hardcoded to HKCU, breaking repair and upgrade component-detection for users other than the installing admin.

.run installer fixes. --prefix is canonicalized to an absolute path (relative prefixes produced dangling symlinks and broken uninstall.sh); upgrades run the previous install’s uninstall.sh first so renamed or removed launchers don’t leave dangling bin symlinks and .desktop entries; payload tar ownership is normalized to root so a root install can’t hand the tree to an unrelated uid; .desktop Name entries are disambiguated when an app installs several.

0.8.0

2026/07/04

Only the installer is emitted in dist/ (breaking). The portable .zip (MSI/MSIX) and the .tar.{gz,bz2,xz} (Linux/macOS) are no longer produced — each target now ships just its installer (.msi/.msix/.run). The --no-zip build option is removed.

Faster .run payload compression. gzip and xz payloads are now compressed through the external gzip/xz commands when available (xz -T0 uses every core) at presets tuned for build speed, falling back to Python’s built-in single-threaded codecs otherwise.

0.7.0

2026/06/10

POSIX launchers are isolated from the host’s PYTHON* environment. The Linux/macOS shell-wrapper launchers ran the bundled interpreter with a plain python3 -c <bootstrap>, so a stray PYTHONHOME/PYTHONPATH (or any PYTHON* variable) in the user’s environment leaked into the app — unlike the Windows and macOS C launchers, which already pass -I and scrub PYTHON*. The wrappers now mirror that isolation: PYTHON* is scrubbed from the environment (so the app and anything it spawns don’t inherit it) and python runs with -I.

Windows launcher build robustness. The vcvars64.bat path is now passed to the generated build.bat as a batch argument, so a Visual Studio install under a non-ASCII path no longer breaks the ASCII-only batch file; the app name embedded in the GUI error-dialog bootstrap is properly escaped; and the launcher itself now fails with exit code 124 when the assembled command line would exceed Windows’ 32768-character limit instead of silently dropping arguments.

Pipeline and installer robustness. build-wheels clears stale wheels from the wheelhouse first, so incremental stage runs can’t hand pip two conflicting versions of a package. The POSIX .run installer verifies the decompressor and tar exist before removing a previous install, and quotes the Exec path in generated .desktop files. notarytool output that isn’t valid JSON now produces a clear error instead of a traceback. The unused packaging dependency was dropped.

0.6.0

2026/06/09

Windows launchers no longer orphan their python child. The launcher placed the python process it spawned outside any job, so terminating the launcher (by a parent process or Task Manager) left python.exe — and anything it had started — running. The launcher now creates a kill-on-close Job Object (JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE), starts the child suspended, assigns it to the job, and only then resumes it, so killing the launcher tears down the whole descendant tree. Job setup is best-effort: any failure falls back to launching the child unmanaged rather than failing the launch.

0.5.0

2026/06/08

Non-ASCII launcher and app names on Windows. The generated build.bat and the launcher .rc were written as ASCII, so a non-ASCII launcher executable name (or app name in the VERSIONINFO) raised UnicodeEncodeError before the toolchain even ran. The build now uses fixed ASCII basenames for every cl/rc input and output — keeping build.bat pure ASCII — and renames the result to its (possibly non-ASCII) name in Python afterwards. The .rc is emitted as UTF-8 prefixed with #pragma code_page(65001) so rc.exe decodes non-ASCII resource strings without depending on the console codepage.

More robust MSVC toolchain discovery. Launcher compilation located MSVC with a bare vswhere -latest, which excluded the standalone “C++ Build Tools” SKU and preview channels and could return an install without the C++ workload. Discovery now mirrors setuptools — adding -products *, -prerelease, and -requires VC.Tools.x86.x64 — raises a clear error when no suitable install is found, and verifies that vcvars64.bat exists.

Documentation. Added a step-by-step Windows installer tutorial (including brief MSVC + WiX setup steps), documented the auto-generated MSI upgrade-code in the examples and tutorial, spelled out the pip wheel and launcher entry-point prerequisites, and corrected stale launcher, pipeline, and output-path descriptions.

0.4.0

2026/06/07

Build intermediates and final artifacts now live in separate trees (breaking). Intermediates (runtime, wheelhouse, image, launcher build, .wxs) go to .appdist-build/<target>/ while the shippable packages go to appdist/<target>/dist/. The two base directories are chosen with the new --build-dir and --appdist-dir options; the old --out-dir is removed. A full build wipes the per-target intermediates directory first for a clean build (the downloaded runtime cache lives elsewhere, so this does not re-download).

Same-version MSI upgrades. A new MSI target key allow-same-version-upgrades (default false) emits AllowSameVersionUpgrades="yes" on the WiX MajorUpgrade, so reinstalling the same version upgrades in place instead of erroring or installing side-by-side — handy while iterating without bumping the version. MSI-only; setting it on an msix target prints a warning and has no effect.

0.3.1

2026/06/07

Windows launcher build no longer breaks when the current directory is excluded from the executable search path. The launcher build step now invokes the generated batch file as .\build.bat instead of a bare build.bat. On systems where NoDefaultCurrentDirectoryInExePath is set, cmd.exe does not search the current directory for the script, so the previous form failed to find it and the MSI build aborted during launcher compilation.

Quieter launcher compile. The Windows launcher C source now uses _snwprintf_s (with _TRUNCATE) instead of the deprecated _snwprintf, removing the MSVC C4996 warning while guaranteeing null-termination on truncation.

0.3.0

2026/06/06

macOS .app / .dmg packages. Two new GUI formats build native macOS bundles: format = "macapp" produces a .app bundle and format = "dmg" wraps it in a disk image. A Mach-O launcher stub is compiled with clang, the .app is assembled with an Info.plist and an icns icon, then deep-codesigned (ad-hoc by default; Developer ID + hardened runtime when signing-identity is set) and, with a notary-profile, notarized via notarytool and stapled. These formats require the app-level identifier (reverse-DNS CFBundleIdentifier) and build only on a macOS host.

Per-OS launcher icons. [[launchers]].icon is now a per-OS table — icon = { windows = "*.ico", macos = "*.png", linux = "*.png" } (each key optional; a plain string is rejected). This replaces the old single .ico and the target-level macOS icon, so each launcher can carry its own icon per OS.

Opt-in MSI code signing. Set code-sign = true on an MSI target to sign the launcher .exe and the .msi. The signing command is resolved with the precedence PYAPPDIST_SIGN_CMD (env) > code-sign-command (config) > a built-in signtool default. MSIX and the macOS .dmg keep their previous environment-variable-only behavior.

Per-target extras. Each target may set an extras list selecting [project.optional-dependencies] extras to bundle. The names are passed through to the lockfile export using the manager’s own flag (uv --extra, poetry --extras, pdm --group, pipenv --categories). The default is empty (production dependencies only); extras are ignored with a warning in requirements.txt mode.

python -m launcher entry. A launcher entry may now be a colon-less dotted "module.path", run as python -m module.path (runpy.run_module with __name__ == "__main__"), alongside the existing "module:callable" form. This packages apps whose startup lives under an if __name__ == "__main__": guard (e.g. NiceGUI) without modification.

Config keys are now kebab-case (breaking). All underscore-separated [tool.pyappdist] keys were renamed to hyphenated form: upgrade-code, code-sign, code-sign-command, identity-name, display-name, min-macos, signing-identity, team-id, notary-profile. Update existing pyproject.toml files accordingly.

Installer reports installed commands. The POSIX self-extracting .run installer now lists the command names it symlinked into <prefix>/bin when it finishes.

0.2.0

2026/06/03

Multiple output formats and targets. A project can now produce several packages from one configuration. The single [tool.pyappdist.wix] table plus a single target has been replaced by a [[tool.pyappdist.targets]] array, where each entry is one output package with its own platform and format. format is now required and is validated against the platform’s OS (msi/msix for Windows, linux for Linux, macos for macOS), so a mismatch is reported as a configuration error at load time instead of failing mid-build.

MSIX output (Microsoft Store). A new format = "msix" produces an unsigned, full-trust Win32 .msix package — emitting AppxManifest.xml (one <Application> per launcher) and packing the image with makeappx. The Store signs the package on ingestion; local installs require Developer Mode.

Linux packages. A new format = "linux" builds a real Linux distribution using relocatable shell-wrapper launchers (no MSVC), and emits two artifacts: a .tar.gz of the image tree and a self-extracting .run installer. The installer is per-user (installs under $HOME/.local, no root and no FUSE), symlinks launchers into bin, and writes a .desktop file for launchers that declare an icon.

macOS packages. A new format = "macos" (platforms macos-aarch64 and macos-x86_64) ships the same per-user tarball + self-extracting .run as Linux, built on the matching macOS host. The default payload compression is gzip (xz is not preinstalled on macOS), and — with no freedesktop equivalent — launcher icon/gui are ignored, so the installer just symlinks launchers into <prefix>/bin.

MSI install scope and licensing. Install scope is now a build-time machine/user choice: scope = "user" (default; installs per-user under %LocalAppData%\Programs) or scope = "machine" (Program Files). The MSI license (EULA) is now optional, and per-user installs get a proper user-folder redirect.

Internals. Runtime extraction now uses the tarfile "tar" filter, and the runtime_source option was dropped.

Docs and samples. Per-format details are now documented as independent sections rather than notes; sample apps were translated to English; an end-to-end editable-install verification project was added; each output format now has its own documentation page; and design and migration notes for macOS support were added.