openappx
Build, sign, bundle and install Windows app packages — from Linux, without Windows tooling.
openappx replaces the makeappx and signtool half of a Windows app pipeline:
it turns a layout directory into a signed .msix or .msixbundle, checks it,
and installs it on a real device over the Windows Device Portal. Pure Python,
standard library only — signing is the one optional extra.
Status: beta. The whole chain works and is verified on hardware: pack → sign → deploy → launch, from Linux, with no Windows tooling. A real 216-translation-unit UWP application — compiled by uwp-crossbuild, packed, signed and installed by this project — has been observed running on an Xbox Series S dev kit (2026-08-08); a real 47 MB UWP application, repackaged and signed here, installs on the same console. Compiling PE/UWP binaries stays out of scope (see Non-goals) — uwp-crossbuild is the companion project that does that part.
pip install openappx # signing needs the extra: openappx[sign]
yay -S python-openappx # Arch, from the AUR
What this is
| You have | openappx gives you |
|---|---|
| A layout directory (manifest, assets, binaries, payload files) | A valid .msix / Appx-style ZIP with AppxBlockMap.xml and [Content_Types].xml |
| A code-signing certificate (or none — one can be minted) | A signed .msix that a real device installs |
A signed .msix from anyone |
Verification that the package matches the digests its signature covers |
| Packages for several architectures | An .msixbundle carrying them all |
| A device in developer mode | Installation over the Windows Device Portal, and the error if it is refused |
| CI on Linux | Deterministic pack tests without Windows |
It is not a replacement for MSBuild and the Windows SDK: it does not compile
anything. It replaces the packaging half — makeappx, signtool, and the
sideload step — of a Windows app pipeline.
Goals
- Pack an Appx/MSIX layout on Linux, macOS, or any host with Python 3.10+
- Generate a conformant
AppxBlockMap(64 KiB SHA-256 blocks, compressed block sizes) — checked against Microsoft-signed reference packages, not guessed - Sign packages without Windows, and prove it by installing them on a device
- Validate common layout mistakes before pack (missing
AppxManifest.xml, missingExecutable, missing logos) - Stay dependency-light (default path: Python standard library only)
- Produce byte-reproducible packages (fixed timestamps; same layout → same
.msix) - Remain product-agnostic: any app that ships as Appx/MSIX layout can use it
Non-goals
| Non-goal | Why |
|---|---|
| Compiling Win32/UWP C++/C# into PE | A separate problem, with a separate tool: uwp-crossbuild |
| Emulating Windows or ReactOS as a build OS | Different problem domain |
| Guaranteeing Store certification | Store has additional policies beyond package shape |
| Replacing a device lab | deploy drives one device over Device Portal; orchestration is yours |
Architecture (host tools)
┌─────────────────────────────────────────────────────────────┐
│ Your app build (elsewhere) │
│ produces: PE/DLLs + assets + AppxManifest.xml │
└────────────────────────────┬────────────────────────────────┘
│ layout directory
▼
┌─────────────────────────────────────────────────────────────┐
│ openappx │
│ validate → blockmap → content types → zip (.msix) │
│ [optional] sign → AppxSignature.p7x → deploy to a device │
└────────────────────────────┬────────────────────────────────┘
│ .msix
▼
┌─────────────────────────────────────────────────────────────┐
│ Install / sideload / store upload (your process) │
└─────────────────────────────────────────────────────────────┘
See docs/architecture.md for the layers and extension points, and docs/format.md for the container and blockmap rules — each recorded with the measurement that established it, since several are not what the specification suggests. Maintainer workflows and documentation ownership are defined in docs/best-practices.md.
Quickstart
# From repo root (no install required)
./scripts/pack.sh --root examples/minimal-layout --out /tmp/example.msix
unzip -l /tmp/example.msix | head
scripts/pack.sh is a thin wrapper that puts src/ on PYTHONPATH. The equivalent
without the wrapper:
PYTHONPATH=src python3 -m openappx.pack --root examples/minimal-layout --out /tmp/example.msix
With an editable install:
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
openappx pack --root examples/minimal-layout --out /tmp/example.msix
pytest -q
Layout contract
A pack root must contain at least:
AppxManifest.xml
…payload files referenced by the manifest (exe, logos, etc.)
openappx writes (do not pre-seed):
[Content_Types].xmlAppxBlockMap.xmlAppxSignature.p7x— written byopenappx sign, never bypack
CLI
openappx pack --root DIR --out FILE.msix [options]
--backend python|makemsix default: python (unsigned)
--makemsix PATH makemsix binary (default: tools/bin/makemsix)
--allow-missing pack even if validation reports gaps
openappx sign --package FILE.msix --pfx CERT.pfx
openappx sign --make-test-cert "CN=Publisher" --cert-out mycert
openappx validate --root DIR # check a layout before packing
openappx unpack --package FILE.msix --out DIR
openappx inspect --package FILE.msix [--json]
openappx deploy --device URL --user NAME --package FILE.msix [--insecure]
openappx deploy --device URL --user NAME --start PACKAGE_FULL_NAME --app-id ID
openappx bundle --package A.msix --package B.msix --out X.msixbundle # repeat --package
openappx --version
A bundle carries one application across architectures, plus any resource packages. Sign the packages first, then the bundle — signing only the container is rejected as if the container were unsigned:
openappx sign --package app-x64.msix --pfx cert.pfx
openappx sign --package app-x86.msix --pfx cert.pfx
openappx bundle --package app-x64.msix --package app-x86.msix --out app.msixbundle
openappx sign --package app.msixbundle --pfx cert.pfx
inspect is the read side of pack: it re-derives every block hash from the bytes
stored in the archive and compares them with AppxBlockMap.xml, checks LfhSize
against the ZIP local headers actually written, and verifies that [Content_Types].xml
covers every part. On a signed package it also recomputes the digests the signature
covers and reports any mismatch — see docs/signing.md. It works on
packages produced by any tool, not just openappx.
Package: /tmp/example.msix (3139 bytes)
Identity: Name=OpenAppx.Example Publisher=CN=OpenAppx-Example Version=0.1.0.0 ProcessorArchitecture=x64
Signature: absent
Part Size Stored Method Blocks
app.exe 31 31 store 1
AppxManifest.xml 1659 737 deflate 1
Assets/StoreLogo.png 67 66 deflate 1
[Content_Types].xml 1061 1061 store -
AppxBlockMap.xml 610 610 store -
OK: blockmap and content types are consistent with the archive
Deploying to a device
deploy talks to the Windows Device Portal,
the same REST service on Xbox, HoloLens, IoT and Windows desktop — so a real
device tells you whether a package is actually installable:
export OPENAPPX_DEVICE_PASSWORD='…' # keeps it out of `ps`
openappx deploy --device https://192.168.1.50:11443 --user devuser \
--package example.msix --insecure
- The device must be in Developer Mode with Device Portal enabled (on Xbox: Dev Home → Home → Remote Access → Remote Access Settings).
--insecureis required because devices serve a self-signed certificate.- CSRF is handled by the cookie-to-header handshake the Device Portal UI uses
(
CSRF-Tokencookie →X-CSRF-Tokenheader).--csrf-bypassswitches to Microsoft'sauto-<username>escape hatch instead; that account must then never be used in the web UI. --listshows installed packages,--uninstall PACKAGE_FULL_NAMEremoves one,--install-cert CERT.certrusts a certificate on the device.--start PACKAGE_FULL_NAME --app-id IDlaunches an installed app (the id isApplication/@Idfrom the manifest);--stop PACKAGE_FULL_NAMEends it.--also-uploadsends dependency packages or a.ceralongside an install;--no-waitskips polling for the result.
Sideloading requires a signed package and a certificate the device trusts. The full loop, entirely from Linux:
openappx sign --make-test-cert "CN=OpenAppx-Example" --cert-out mycert
openappx deploy --device https://<ip>:11443 --user NAME --install-cert mycert.cer --insecure
openappx pack --root examples/resource-only --out app.msix
openappx sign --package app.msix --pfx mycert.pfx --timestamp
openappx deploy --device https://<ip>:11443 --user NAME --package app.msix --insecure
examples/resource-only/ is the layout this loop was verified with: it installs
on an Xbox Series S dev kit. examples/minimal-layout/ shows a desktop full-trust
manifest instead, and deliberately ships a placeholder executable, so it packs
and signs but stops at the deployment stage.
Signing needs pip install 'openappx[sign]'; everything else is stdlib-only.
The same extra is needed when inspect must read certificate details from a
signed package; digest and archive checks remain available without it.
docs/signing.md has the format details and the console
responses that verify each step.
Exit codes: 0 success, 1 a valid command failed at runtime or found invalid
content, 2 bad usage, a missing/unreadable input path, or a layout that fails
validation (pack without --allow-missing). For example, validate returns
1 for layout problems, while unpack returns 1 for a corrupt archive and
2 when the package path does not exist.
Without an editable install, replace openappx pack with
PYTHONPATH=src python3 -m openappx.pack (same for validate and inspect).
Optional: makemsix backend
Microsoft’s open-source MSIX SDK can pack on Linux when built with pack support:
./scripts/bootstrap-makemsix.sh # may fail on bleeding-edge toolchains
openappx pack --backend makemsix --root … --out …
It produces unsigned packages, exactly like the Python backend: makemsix pack
takes only -d/-p, and upstream implements signature validation, not creation.
See docs/signing.md. The pure-Python backend remains the default
and is the one covered by the test suite.
Project layout
openappx/
├── src/openappx/
│ ├── __init__.py # the version, and nothing else
│ ├── __main__.py # `python -m openappx`
│ ├── cli.py # the `openappx` entry point; dispatches lazily
│ ├── blockmap.py # block hashing, XML rendering, ZIP header parsing
│ ├── pack_core.py # the ZIP writer and the two pack backends
│ ├── pack.py # pack CLI
│ ├── bundle.py # .msixbundle assembly
│ ├── validate.py # pre-pack layout checks
│ ├── inspect.py # post-pack package and bundle checks
│ ├── unpack.py # extract a layout back out of a package
│ ├── deploy.py # Windows Device Portal client
│ └── sign/ # digests, DER encoder, signature creation
├── docs/
│ ├── architecture.md # the layers and where to extend them
│ ├── format.md # container and blockmap rules, with their evidence
│ ├── signing.md # what AppxSignature.p7x contains, decoded
│ ├── best-practices.md # doc and release hygiene rules this repo holds itself to
│ ├── audit-2026-08.md # the 2026-08 repo audit and its verification evidence
│ └── roadmap.md # done, not done, and why
├── examples/
│ ├── minimal-layout/ # desktop, full-trust; placeholder exe, so it never installs
│ └── resource-only/ # installs on a device — used to prove the chain
├── tests/
└── scripts/
Known limits
| Limit | Detail |
|---|---|
| Files above 4 GiB | Cannot be carried. Describing one needs ZIP64 extra fields on the record, and a device refuses a package with those (0x8007000B) — see docs/format.md. pack fails with that explanation. |
| Memory | pack builds the archive in memory: fine for tens of MB, not for GB. |
| Running an app | A package packed and signed here has been launched and observed running on an Xbox Series S via deploy --start (0.6.3 — earlier releases built the launch request wrong, which for a while read as a console that refused everything). One console, one OS build. |
| Timestamping | --timestamp implemented; that Windows honours it past certificate expiry is untestable here. |
| Bundles | A bundle mixing an application and a language pack registers only if both resources.pri merge; ours do not yet (0x80070002). |
| CodeIntegrity | AppxMetadata/CodeIntegrity.cat is verified when present, never generated. It matters only where Device Guard is enforced. |
| Certificate trust | inspect reports the signer and checks publisher agreement and expiry, but never the chain of trust. |
Roadmap
Pack, sign, bundle and deploy are done and verified on hardware. What remains is
in docs/roadmap.md, with the reason for each: streaming pack,
CodeIntegrity.cat, and merged resource bundles.
Releasing
Tagging is the whole of it — everything downstream keys off the tag:
git tag -a v<version> -m "…" && git push origin v<version>
release.ymlrefuses to continue if the tag andpyproject.tomldisagree, builds, runstwine check, installs the built wheel and drives the CLI through pack/sign/inspect, attests build provenance, and uploads to PyPI via Trusted Publishing. No API token lives in this repository.aur.ymlthen updates the AUR package — triggered by the release finishing rather than by the tag, because the PKGBUILD builds from the PyPI sdist and would otherwise race the upload.
The AUR step runs packaging/publish-aur.sh, which is also the manual path:
packaging/publish-aur.sh --version <version> --dry-run # build and check only
packaging/publish-aur.sh --version <version> # and push to the AUR
It rewrites pkgver, downloads the sdist to compute its checksum,
regenerates .SRCINFO and builds the package with its tests before pushing —
so a hand-edited checksum can never describe a different file. Anyone can check
what built a release:
gh attestation verify openappx-<version>.tar.gz --repo gianlucamazza/openappx
Contributing
See CONTRIBUTING.md for setup and the checks CI runs, and SECURITY.md for how keys, device credentials and untrusted archives are handled.
The short version: keep the default pack path dependency-free, and when a format detail is in doubt, verify it against a real package or a real device rather than against the specification — that is how every serious bug here was found.
License
MIT — see LICENSE.
Release files for openappx 0.6.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| openappx-0.6.4.tar.gz | 104.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| openappx-0.6.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 161.7 kB
Release files / openappx-0.6.4.tar.gz
| Download URL | openappx-0.6.4.tar.gz |
|---|---|
| Size | 104.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ecd0ae76d2aa1ecefc892845ac6eca9fc9d6c763cbeb965819868c34e11a26cb
|
|
BLAKE2b-256 checksum How to use checksums |
13c69f1e2e3db7641dcbdba4b4ec92090e10c43c2d7baca31ae9b94ec0303f1e
|
| 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 Aug 9, 2026.
Transparency logRelease files / openappx-0.6.4-py3-none-any.whl
| Download URL | openappx-0.6.4-py3-none-any.whl |
|---|---|
| Size | 57.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a5ef0b7c9c4cc5585027e9d2734880009357f2e582f97ab7ec783048b5ae72c0
|
|
BLAKE2b-256 checksum How to use checksums |
5ead8e3c5d204eecae81c8a7835deea724d4054ab2225cfe203832b07715d95d
|
| 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 Aug 9, 2026.
Transparency log