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, from Linux, with no Windows tooling. A real 47 MB UWP application, repackaged and signed by this project, installs on an Xbox One dev kit. 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 bundle --package A.msix --package B.msix --out X.msixbundle # repeat --package
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.
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 One 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 or a missing/unreadable input path. 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/
│ ├── 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
│ └── 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 | Packages are proven to install. None has been seen to run: the test console refuses to launch every sideloaded package, Microsoft Edge included, so it cannot answer the question either way. |
| 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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openappx-0.6.2.tar.gz.
File metadata
- Download URL: openappx-0.6.2.tar.gz
- Upload date:
- Size: 101.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4aa8cb74cdee889d3af5f301b57b74d5cbb1deb5b410d5bfb3229636c4c1e2e1
|
|
| MD5 |
2314cfffe6f346b1cd7d468590262d30
|
|
| BLAKE2b-256 |
2a49ef5b82b2e8677bc8904905793944c7f99048f2c3dd05565ab034e7e0c86a
|
Provenance
The following attestation bundles were made for openappx-0.6.2.tar.gz:
Publisher:
release.yml on gianlucamazza/openappx
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openappx-0.6.2.tar.gz -
Subject digest:
4aa8cb74cdee889d3af5f301b57b74d5cbb1deb5b410d5bfb3229636c4c1e2e1 - Sigstore transparency entry: 2311268382
- Sigstore integration time:
-
Permalink:
gianlucamazza/openappx@0b90371ba8a2e934e23cf6f57175cb0fc22433b9 -
Branch / Tag:
refs/tags/v0.6.2 - Owner: https://github.com/gianlucamazza
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0b90371ba8a2e934e23cf6f57175cb0fc22433b9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openappx-0.6.2-py3-none-any.whl.
File metadata
- Download URL: openappx-0.6.2-py3-none-any.whl
- Upload date:
- Size: 56.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cce305ef09207dff3dd9b8086c1ce07f69a3d52cdd71e26c33772761a36101a4
|
|
| MD5 |
cdb1d48fd04d7027a16c41f8d3f78558
|
|
| BLAKE2b-256 |
487d6e5cd5a56bc7b07039660073edac48b85092c2891e00eb69ef6322cd761c
|
Provenance
The following attestation bundles were made for openappx-0.6.2-py3-none-any.whl:
Publisher:
release.yml on gianlucamazza/openappx
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openappx-0.6.2-py3-none-any.whl -
Subject digest:
cce305ef09207dff3dd9b8086c1ce07f69a3d52cdd71e26c33772761a36101a4 - Sigstore transparency entry: 2311268520
- Sigstore integration time:
-
Permalink:
gianlucamazza/openappx@0b90371ba8a2e934e23cf6f57175cb0fc22433b9 -
Branch / Tag:
refs/tags/v0.6.2 - Owner: https://github.com/gianlucamazza
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@0b90371ba8a2e934e23cf6f57175cb0fc22433b9 -
Trigger Event:
push
-
Statement type: