pyxbuild
Build a Windows x64 application from a standard Python project — on macOS, Linux or Windows. No compiler, no Wine, no virtual machine. Linux x86_64 and aarch64 applications are built the same way, from any of the three.
$ pyxbuild build
[win_amd64] Python 3.12 runtime (python-build-standalone)
[win_amd64] building wheel for myapp
[win_amd64] resolving 2 requirement(s) with uv
[win_amd64] installing 14 wheel(s)
[win_amd64] built dist/myapp-win_amd64
dist/myapp-win_amd64/
├── myapp.exe ← launcher (one per entry point)
└── runtime/ ← CPython for Windows + your wheels in Lib/site-packages
Copy the folder (or the --zip) to any Windows 10/11 x64 machine; no Python
or Visual C++ Redistributable needs to be installed.
Try it in the browser: edit a
small project and download the .exe, from any device (Android and iPad
included), nothing to install.
How it works
Building an .exe (or a Linux executable) here means assembling, not compiling:
- download a prebuilt CPython for the target (python-build-standalone, pinned by version and sha256);
- resolve dependencies for the target (e.g.
sys_platform == "win32",platform_machine == "AMD64") withuv pip compile --python-platform, written to a standardpylock.toml(PEP 751); - install the target's wheels (
win_amd64,manylinux_*) into the runtime'ssite-packageswith the PyPAinstallerlibrary — no code from the wheels is run; - copy a small prebuilt launcher (C, compiled once with
zig ccand shipped inside pyxbuild: PE for Windows, static ELF for Linux) and append a manifest naming the entry point.
The result is byte-for-byte the same on every build host.
Installation
pip install pyxbuild # or: uv tool install pyxbuild / pipx install pyxbuild
Python 3.11+ on Windows, macOS or Linux.
Usage
[project]
name = "myapp"
version = "1.2.0"
requires-python = ">=3.12"
dependencies = ["requests>=2.32", "PySide6"]
[project.gui-scripts]
myapp = "myapp.main:run"
[tool.pyxbuild] # everything is optional
targets = ["win_amd64"] # also "linux_x86_64", "linux_aarch64"
python = "3.12" # default: newest supported allowed by requires-python
mode = "folder" # or "onefile": a single .exe (see below)
lock = "pylock.toml" # default: pylock.win_amd64.toml
exclude = ["tests/", "*.pyi"]
zip = true
utf8-mode = false # run the app with -X utf8
icon = "assets/app.ico" # launcher icon (or a list of PNGs); VERSIONINFO from [project]
include = ["assets/**", "LICENSE"] # project files copied next to the app ({app_dir})
find-links = ["wheels/"] # extra local wheels (e.g. built in CI)
slim = ["tests", "idle", "ensurepip", "turtledemo", "lib2to3", "tk"] # drop stdlib parts
bytecode = true # precompiled .pyc: fast first start, read-only folders
bytecode-optimize = 2 # .pyc without asserts (1) and docstrings (2)
strip-sources = true # ship your own modules as .pyc only (implies bytecode)
pure-python = ["msgpack"] # build without the optional C extension (no wheel case)
cross-compile = ["markupsafe"] # experimental: compile C extensions with zig
app-id = "Acme.MyApp" # GUI: taskbar identity (AppUserModelID)
truststore = true # use the system certificate store for TLS
gui-log = "{data_dir}/logs/%Y-%m-%d.log" # GUI apps' stdout/stderr; false: none
[tool.pyxbuild.env] # set at start-up; {app_dir} {data_dir} {localappdata}
NLTK_DATA = "{app_dir}\\nltk_data"
[tool.pyxbuild.version-info]
company-name = "ACME"
copyright = "(c) 2026 ACME"
[tool.pyxbuild.build-info] # stored in runtime/pyxbuild.json for the app to read
channel = "beta"
pyxbuild build [--target win_amd64|linux_x86_64|linux_aarch64] [--python 3.12] [--zip] [--frozen | --offline]
pyxbuild lock # resolve only, (re)write pylock.win_amd64.toml
pyxbuild doctor # missing wheels, missing DLLs, VC++ runtime, long paths
pyxbuild sign dist/myapp-win_amd64 --cert cert.pfx --password-env PFX_PASSWORD
Commit the lock file: pyxbuild build --frozen then never resolves again, and
--offline builds from the download cache only.
Assets, sources and build info
includecopies project files that are not part of the package (game assets,LICENSE, config templates) to the same relative path in the application folder; read them through{app_dir}, e.g.[tool.pyxbuild.env] GAME_ASSETS = "{app_dir}/assets". Patterns are gitignore-style and relative to the project folder;.git, virtual environments and the output folder are skipped.strip-sources = truekeeps only.pycfiles for your own modules (dependencies keep their sources). It hides the code from a casual look and nothing more:.pycfiles decompile.bytecode-optimize = 2also drops asserts and docstrings.slimalso knows engine presets that drop files an application never uses:panda3d-tools(Panda3D's model converters, ~57 MB) andarcade-assets(Arcade's sample:resources:images, sounds and examples, ~18 MB; its fonts and shaders stay). macOS.dylibfiles that some wheels carry are never installed for Windows or Linux (Arcade: 59 MB of ffmpeg).icontakes an.icoor a list of PNG files (each up to 256×256; give 16, 32, 48 and 256 px), stored in the launcher as they are.[tool.pyxbuild.build-info]is stored as"build-info"inruntime/pyxbuild.json(os.path.join(sys.prefix, "pyxbuild.json")), e.g. a release channel or commit for an About box.
Linux applications
--target linux_x86_64 / --target linux_aarch64 (or targets = [...])
build the same project for Linux, from macOS, Linux or Windows:
dist/myapp-linux_x86_64/
├── myapp ← launcher (static ELF, one per entry point)
└── runtime/ ← CPython for Linux + your wheels in lib/python3.X/site-packages
- Runs on glibc 2.28 or newer (RHEL/Rocky 8, Debian 10, Ubuntu 20.04 and later);
wheels are chosen from
manylinux_2_28down tomanylinux1. zip = truewrites a.tar.gz(it keeps executable bits);mode = "onefile"gives one executable that extracts once to~/.cache/pyx/<app>/<hash>($PYX_ONEFILE_DIR,$XDG_CACHE_HOMErespected;onefile-extract = "temp"uses$TMPDIRand cleans up). Concurrent first starts extract once, and old versions are removed unless a copy is still running.- Console scripts of dependencies (
runtime/bin/*) are relocatable shell/Python scripts, so the folder can be moved anywhere. {data_dir}is$XDG_DATA_HOME/<app>(default~/.local/share/<app>).- Windows-only, ignored with a warning for Linux targets:
icon,version-info,app-id,onefile-progress,cross-compile; also not for Linux: the error message box, patch executables, signing and the DLL check.
In the browser
The sandbox runs the real
pyxbuild in Pyodide inside a web worker: no processes
there, so uv is replaced by a built-in resolver (resolvelib over PyPI's JSON
API and PEP 658 metadata; same versions as uv in our checks) and the project
wheel is packed in-process (static [project] metadata, src/<pkg>,
<pkg>/ or <module>.py). Wheels and the Windows runtime are downloaded by
the browser; the runtimes are served by the site because GitHub release
downloads do not allow cross-origin requests. The built-in resolver also works
on the command line: PYXBUILD_RESOLVER=python pyxbuild build.
GitHub Actions
Build on any runner and get the application as a workflow artifact:
on: [push, workflow_dispatch] # workflow_dispatch: a "Run workflow" button
jobs:
windows-app:
runs-on: ubuntu-latest # or macos-latest, windows-latest
steps:
- uses: actions/checkout@v4
- uses: sedrew/pyxbuild@main
with:
project: . # folder with pyproject.toml
args: --mode onefile # any `pyxbuild build` options
artifact-name: windows-app
The action installs pyxbuild (pyxbuild-version, default: the action's own
commit), caches the downloaded runtime and wheels (cache: true), builds into
a temporary folder (output dist) and uploads it (upload: true). Edit code
and press "Run workflow" from the GitHub website or app, on any device, and
download a ready .exe.
One file
mode = "onefile" (or pyxbuild build --mode onefile) produces
dist/<script>.exe alone: the application folder travels inside the
executable as a compressed archive. Windows can only load DLLs and .pyd
files from disk, so the launcher extracts it:
onefile-extract = "cache"(default): once per version, into%LOCALAPPDATA%\pyx\<app>\<hash>\; later starts reuse it and are as fast as the folder mode. Extraction goes to a temporary folder that is renamed when complete, under a named mutex, so concurrent or interrupted first starts are safe; the previous version is kept, older ones are removed.PYX_ONEFILE_DIRchanges the location,PYX_ONEFILE_RESET=1re-extracts.onefile-extract = "temp": like PyInstaller, into%TEMP%on every start, deleted when the application exits (folders of killed runs are swept on a later start).
GUI apps show a small progress window while the first extraction runs
(only if it takes longer than 0.4 s; PYX_ONEFILE_PROGRESS=0 turns it off).
By default it is drawn in the Windows 11 style: the app's icon and name
(version-info product-name, else the project name), the user's accent
color, light or dark like Windows, sharp on scaled displays, rounded corners
on Windows 11; it never takes the focus. onefile-progress = "classic" shows
a plain box with the percentage instead, "off" nothing. Under Wine:
Combine it with bytecode = true for a fast first start. {app_dir} in
[tool.pyxbuild.env] is the extracted folder (bundled data), {exe_dir} the
folder of the .exe. pyxbuild sign dist/myapp.exe signs it, archive
included. The launcher stays free of the C runtime (19 KB, 27 KB with the GUI window; its own DEFLATE
decoder).
Updates (patch executables)
Users of a onefile app (cache mode) do not need to download the whole new executable again:
pyxbuild patch --from v1/myapp.exe --to dist/myapp.exe # -> myapp-update-2.0.exe
writes a small executable with only what changed: files whose content the
installed version already has (at any path) are reused, the rest travels in
the patch. With --fetch-public the files of public wheels from the lock file
(--lock, default pylock.win_amd64.toml) are not carried; the patch
downloads those wheels from their URLs when it runs and checks their sha256.
Your own code and private/local wheels always travel inside the patch.
Running the patch builds the new version next to the installed one (reused
files are hard-linked after checking their hashes; every written file is
checked), with the installed version's Python, so the result is the folder
the new full executable would extract. The old myapp.exe then starts the
new version, and the new myapp.exe finds it ready. A patch for a version
that is not installed on that computer says so and changes nothing. A change
of only your code makes a patch of a few tens of KB.
Monorepos
Dependencies on sibling packages through uv's [tool.uv.sources] work:
{ workspace = true } (members of the uv workspace above the project) and
{ path = "../tools" } (a folder or a .whl). pyxbuild builds those packages
from the checkout like the project itself, installs them next to it and
resolves only their dependencies, so the lock never names them and stays
portable. directory entries of a PEP 751 lock exported by another tool
(uv export --format pylock.toml) are built the same way. Local packages must
be pure Python (a package with compiled code: build its win_amd64 wheel in
CI and add it through find-links).
Files found through find-links are written to the lock as paths relative to
the lock file, also above the project (find-links = ["../../wheels"] →
path = "../../wheels/x.whl"), so --frozen works on any checkout with the
same layout. Only a folder on another Windows drive is kept as an absolute
file:// URL.
Limits
- Every dependency needs a wheel for the target (
win_amd64ormanylinux_*,abi3included) or a pure-Python one on the index, or one of the fallbacks in docs/NO_WHEEL.md (pure-python, Windows-CI wheels viafind-links, experimentalcross-compilewith zig — 14 of 19 tested sdists build that way). - The project itself must be pure Python.
- Some wheels need Visual C++ runtime DLLs that Windows does not ship
(torch, ...).
pyxbuild doctorlists them; addmsvc-runtimeto the dependencies to bundle them. - Targets:
win_amd64,linux_x86_64,linux_aarch64(glibc 2.28+). No macOS yet; the Windows-only features are listed under Linux applications.
Documentation: design · testing · compatibility matrix (38 packages × Python 3.11–3.14) · real applications (Django, FastAPI, Streamlit, JupyterLab, zig cross-compiled extensions, … — 22 of 23 work) · no wheel for the target? · real-world issues and coverage · benchmarks vs PyInstaller · Go / TypeScript comparison · open questions.
pyxbuild doctor also checks the project's own imports (undeclared
dependencies, POSIX-only modules such as fcntl), DLLs and the VC++ runtime,
several OpenMP runtimes, packages held back for lack of wheels, and packages
known to download data or compile at run time.
Development
pip install -e ".[dev,stubs]"
pytest -m "not slow" # unit tests, seconds
eval "$(tools/get_wine.sh)" # Wine 11 for running apps on Linux
pytest -m "slow and not golden" # build + run real apps
pytest -m golden # golden set (numpy, pandas, Qt, ...)
python tools/build_stubs.py --check # committed launchers == source
Metadata
Release files for pyxbuild 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyxbuild-0.1.0.tar.gz | 335.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyxbuild-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 556.9 kB
Release files / pyxbuild-0.1.0.tar.gz
| Download URL | pyxbuild-0.1.0.tar.gz |
|---|---|
| Size | 335.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
da135af085f57c953437ba8a06af7fe3ccc91ac8c111bb0771e71522f8dac444
|
|
BLAKE2b-256 checksum How to use checksums |
6a0f1ca0ef6217475fffa8676b5d6bc94b479cf7a1c2196e2b2ab679de3bc497
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logRelease files / pyxbuild-0.1.0-py3-none-any.whl
| Download URL | pyxbuild-0.1.0-py3-none-any.whl |
|---|---|
| Size | 221.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
90f28ed87d20ee21dd9ecce85e7102463f2847f8f283bd374b56cc43cff685a4
|
|
BLAKE2b-256 checksum How to use checksums |
a91d950faef964281963a5ccc20174d52b2379c5d121db1806a51856f48968a3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency log