openappx 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openappx-0.6.0/CHANGELOG.md +183 -0
- openappx-0.6.0/CONTRIBUTING.md +52 -0
- openappx-0.6.0/LICENSE +21 -0
- openappx-0.6.0/MANIFEST.in +8 -0
- openappx-0.6.0/PKG-INFO +345 -0
- openappx-0.6.0/README.md +312 -0
- openappx-0.6.0/SECURITY.md +38 -0
- openappx-0.6.0/docs/architecture.md +64 -0
- openappx-0.6.0/docs/format.md +176 -0
- openappx-0.6.0/docs/roadmap.md +142 -0
- openappx-0.6.0/docs/signing.md +203 -0
- openappx-0.6.0/examples/minimal-layout/AppxManifest.xml +47 -0
- openappx-0.6.0/examples/minimal-layout/Assets/StoreLogo.png +0 -0
- openappx-0.6.0/examples/minimal-layout/app.exe +1 -0
- openappx-0.6.0/examples/resource-only/AppxManifest.xml +36 -0
- openappx-0.6.0/examples/resource-only/Assets/StoreLogo.png +0 -0
- openappx-0.6.0/pyproject.toml +67 -0
- openappx-0.6.0/setup.cfg +4 -0
- openappx-0.6.0/src/openappx/__init__.py +3 -0
- openappx-0.6.0/src/openappx/__main__.py +3 -0
- openappx-0.6.0/src/openappx/blockmap.py +240 -0
- openappx-0.6.0/src/openappx/bundle.py +346 -0
- openappx-0.6.0/src/openappx/cli.py +56 -0
- openappx-0.6.0/src/openappx/deploy.py +482 -0
- openappx-0.6.0/src/openappx/inspect.py +506 -0
- openappx-0.6.0/src/openappx/pack.py +91 -0
- openappx-0.6.0/src/openappx/pack_core.py +358 -0
- openappx-0.6.0/src/openappx/sign/__init__.py +40 -0
- openappx-0.6.0/src/openappx/sign/asn1.py +112 -0
- openappx-0.6.0/src/openappx/sign/certificate.py +116 -0
- openappx-0.6.0/src/openappx/sign/cli.py +118 -0
- openappx-0.6.0/src/openappx/sign/digest.py +173 -0
- openappx-0.6.0/src/openappx/sign/signer.py +363 -0
- openappx-0.6.0/src/openappx/sign/timestamp.py +127 -0
- openappx-0.6.0/src/openappx/unpack.py +90 -0
- openappx-0.6.0/src/openappx/validate.py +169 -0
- openappx-0.6.0/src/openappx.egg-info/PKG-INFO +345 -0
- openappx-0.6.0/src/openappx.egg-info/SOURCES.txt +55 -0
- openappx-0.6.0/src/openappx.egg-info/dependency_links.txt +1 -0
- openappx-0.6.0/src/openappx.egg-info/entry_points.txt +2 -0
- openappx-0.6.0/src/openappx.egg-info/requires.txt +9 -0
- openappx-0.6.0/src/openappx.egg-info/top_level.txt +1 -0
- openappx-0.6.0/tests/conftest.py +90 -0
- openappx-0.6.0/tests/test_bundle.py +348 -0
- openappx-0.6.0/tests/test_certificate_timestamp.py +210 -0
- openappx-0.6.0/tests/test_cli.py +128 -0
- openappx-0.6.0/tests/test_deploy.py +403 -0
- openappx-0.6.0/tests/test_docs.py +112 -0
- openappx-0.6.0/tests/test_format.py +211 -0
- openappx-0.6.0/tests/test_inspect.py +219 -0
- openappx-0.6.0/tests/test_makemsix_backend.py +149 -0
- openappx-0.6.0/tests/test_pack.py +177 -0
- openappx-0.6.0/tests/test_sign_cli.py +129 -0
- openappx-0.6.0/tests/test_signature.py +220 -0
- openappx-0.6.0/tests/test_signer.py +220 -0
- openappx-0.6.0/tests/test_unpack.py +131 -0
- openappx-0.6.0/tests/test_zip64.py +133 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes per release. Dates are the day the work landed.
|
|
4
|
+
|
|
5
|
+
## 0.6.0 — 2026-08-01
|
|
6
|
+
|
|
7
|
+
First release published to PyPI.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`openappx bundle`** — combine `.msix` packages into an `.msixbundle`, the
|
|
12
|
+
last big gap in the tool. A bundle built and signed here installs on an Xbox
|
|
13
|
+
One dev kit. `inspect` reads bundles too, and reports Microsoft's own
|
|
14
|
+
deliberately-broken reference bundles exactly as upstream labels them.
|
|
15
|
+
|
|
16
|
+
Five things differ from a package, and every one was found by a device
|
|
17
|
+
refusing a bundle that looked correct — see [docs/format.md](docs/format.md):
|
|
18
|
+
- the signature uses a **different SIP GUID**. A bundle signed with the
|
|
19
|
+
package GUID verifies locally and is rejected as unsigned.
|
|
20
|
+
- **every payload in a signed bundle must be signed too.** The device blames
|
|
21
|
+
the bundle, so the error points at the wrong file.
|
|
22
|
+
- a package is a resource package because `Identity/@ResourceId` is set, not
|
|
23
|
+
because it has no `<Applications>`.
|
|
24
|
+
- application packages carry **no** `ResourceId`; one there stops the device
|
|
25
|
+
matching them by architecture.
|
|
26
|
+
- `Package/@Offset` is where the payload's _data_ starts, not its record.
|
|
27
|
+
|
|
28
|
+
- `inspect` rejects an `Application/@Executable` that is not linked for the app
|
|
29
|
+
container. MSBuild sets `IMAGE_DLLCHARACTERISTICS_APPCONTAINER` from
|
|
30
|
+
`<AppContainerApplication>`; cross-compiling you have to ask the linker for
|
|
31
|
+
it, and forgetting is silent — the binary still runs on a desktop. The flag is
|
|
32
|
+
readable from the PE stored in the archive, so this is a local check. It
|
|
33
|
+
reports only the confident no: an `Executable` that is not a parseable PE is
|
|
34
|
+
left alone.
|
|
35
|
+
- `validate` reports a managed `EntryPoint="ns.Class"` with no `ns.winmd` in the
|
|
36
|
+
layout. The activation lookup needs that file; without it the package
|
|
37
|
+
installs and then refuses to start, which reads like an application bug.
|
|
38
|
+
- `validate` reports build artefacts (`*.obj`, `*.pch`, `*.pdb`, …) in a layout.
|
|
39
|
+
Everything in the directory gets packed, and a stray precompiled header is
|
|
40
|
+
around 190 MB.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- The manifest greps no longer match the manifest's own comments. These files
|
|
45
|
+
document themselves, and the attributes worth explaining are exactly the ones
|
|
46
|
+
being searched for, so a comment mentioning `Executable=` was read as markup.
|
|
47
|
+
|
|
48
|
+
### Established
|
|
49
|
+
|
|
50
|
+
- `0x8d160120` on app launch is a property of this console, not of any packer —
|
|
51
|
+
and this is now settled rather than suspected. Device Portal refuses to launch
|
|
52
|
+
**every** sideloaded package on it, **Microsoft Edge included**. Four packages
|
|
53
|
+
with nothing in common but the hardware behave identically: xllama built on
|
|
54
|
+
Windows with MSBuild, the same xllama repackaged here, a UWP application
|
|
55
|
+
compiled from scratch on Linux, and Edge as Microsoft signed and shipped it.
|
|
56
|
+
All install, none launches, same code.
|
|
57
|
+
|
|
58
|
+
This retires an open question, and does not replace it with a better one: it
|
|
59
|
+
says nothing about whether a repackaged application runs correctly once
|
|
60
|
+
started, only that this particular failure cannot be read as evidence.
|
|
61
|
+
|
|
62
|
+
## 0.5.0 — 2026-08-01
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- `openappx sign --timestamp` — RFC 3161 countersignature, so a signature
|
|
67
|
+
outlives its certificate. The token covers the RSA signature (Authenticode's
|
|
68
|
+
arrangement) and `openssl ts -verify` accepts it against exactly those bytes;
|
|
69
|
+
a timestamped package still installs. Unlike the rest of the format work this
|
|
70
|
+
was not copied from a reference package — neither available Microsoft package
|
|
71
|
+
is timestamped — so what remains unproven is that Windows honours it after
|
|
72
|
+
expiry.
|
|
73
|
+
- `inspect` now reports the signer: subject, self-signed or issued, expiry, and
|
|
74
|
+
whether a timestamp is present. It checks publisher agreement and validity
|
|
75
|
+
dates, never chain of trust.
|
|
76
|
+
|
|
77
|
+
- `openappx unpack` — extract a layout a packer can consume again, the missing
|
|
78
|
+
half of repackaging. Round-trips a real 19 MB package to byte-identical
|
|
79
|
+
output. Archive member names are validated, so a crafted package cannot write
|
|
80
|
+
outside the destination.
|
|
81
|
+
- `openappx deploy --start/--stop` — launch and stop apps through
|
|
82
|
+
`/api/taskmanager/app`.
|
|
83
|
+
- [docs/format.md](docs/format.md) — the container and blockmap rules in one
|
|
84
|
+
place, each with the measurement that established it.
|
|
85
|
+
- `CONTRIBUTING.md`, `SECURITY.md`, Dependabot, `.editorconfig`.
|
|
86
|
+
|
|
87
|
+
### Fixed
|
|
88
|
+
|
|
89
|
+
- **Non-ASCII entry names were corrupted.** Names were written as UTF-8 without
|
|
90
|
+
flag bit 11, so every reader decoded them as CP437: `città-日本.png` became
|
|
91
|
+
`città-日本.png`.
|
|
92
|
+
- **Files above 4 GiB now fail with an explanation.** Describing one needs ZIP64
|
|
93
|
+
extra fields on the record, and a package carrying those is refused by a
|
|
94
|
+
device with `0x8007000B` — the same code a ZIP32 archive gets, measured
|
|
95
|
+
against a package that installs without them. Appx wants the ZIP64
|
|
96
|
+
end-of-central-directory and nothing on the entries.
|
|
97
|
+
- Nine type errors, mypy never having been run: a dataclass holding the
|
|
98
|
+
certificate and private key as bare `object`, and a check that summed values
|
|
99
|
+
it had just established could be `None`.
|
|
100
|
+
- An empty `--uninstall` value fell through to the install branch and crashed.
|
|
101
|
+
|
|
102
|
+
### Testing and CI
|
|
103
|
+
|
|
104
|
+
- The two command-line entry points had no tests at all; they now have 26.
|
|
105
|
+
Coverage 90% → 95%, tests 106 → 170.
|
|
106
|
+
- CI installs `[dev,sign]` — without it every signature test skipped itself —
|
|
107
|
+
and now runs ruff, mypy and a coverage gate.
|
|
108
|
+
- `actions/checkout` v4 → v5, `setup-python` v5 → v6, Python matrix gains 3.14.
|
|
109
|
+
- The makemsix backend, previously the only code nobody had ever run, is covered
|
|
110
|
+
by a stub binary.
|
|
111
|
+
|
|
112
|
+
### Known limits
|
|
113
|
+
|
|
114
|
+
- A repackaged application has been proven to **install**, not to **run**:
|
|
115
|
+
launching it fails on the console, but so does the original Windows-built
|
|
116
|
+
package, so the two are at parity and neither has been seen to start.
|
|
117
|
+
- No timestamping, so signatures expire with the certificate.
|
|
118
|
+
- No `.msixbundle`, and `CodeIntegrity.cat` is read but not generated.
|
|
119
|
+
- `pack` holds the archive in memory; fine at 47 MB, not at 2 GB.
|
|
120
|
+
|
|
121
|
+
## 0.4.0 — 2026-08-01
|
|
122
|
+
|
|
123
|
+
The whole chain now works from Linux, with no Windows tooling: **pack → sign →
|
|
124
|
+
deploy**. A real 47 MB UWP application, repackaged and signed by this project,
|
|
125
|
+
installs on an Xbox One dev kit.
|
|
126
|
+
|
|
127
|
+
### Added
|
|
128
|
+
|
|
129
|
+
- `openappx sign` — creates `AppxSignature.p7x`. The CMS/PKCS#7 structure was
|
|
130
|
+
copied from a decoded Microsoft signature; the DER encoder is ours
|
|
131
|
+
(`sign/asn1.py`). `cryptography` sits behind the optional `[sign]` extra for
|
|
132
|
+
RSA and PKCS#12; packing, inspecting and verifying stay stdlib-only.
|
|
133
|
+
- `openappx sign --make-test-cert` — mint a self-signed certificate whose subject
|
|
134
|
+
matches `Identity/@Publisher`.
|
|
135
|
+
- `openappx deploy` — Windows Device Portal client (install, list, uninstall,
|
|
136
|
+
`--install-cert`). The same REST API serves Xbox, HoloLens, IoT and desktop.
|
|
137
|
+
- `openappx inspect` — contents, blockmap summary and signature verification for
|
|
138
|
+
any `.msix`, not only ours (`--json` for machine output).
|
|
139
|
+
- `openappx.sign.digest` — parse the `APPX` digest blob and recompute
|
|
140
|
+
`AXPC`/`AXCD`/`AXCT`/`AXBM`/`AXCI` from an archive.
|
|
141
|
+
- `examples/resource-only/` — a layout that genuinely installs on a device.
|
|
142
|
+
- Golden tests against Microsoft's own signed packages, fetched on demand
|
|
143
|
+
(`OPENAPPX_NO_NETWORK=1` to skip).
|
|
144
|
+
|
|
145
|
+
### Fixed — format conformance
|
|
146
|
+
|
|
147
|
+
Comparing our output with a Microsoft-signed package, and then with what a
|
|
148
|
+
console accepts, exposed four ways the packer was wrong:
|
|
149
|
+
|
|
150
|
+
- **Appx archives must be ZIP64.** Not a size question: a ZIP32 package of any
|
|
151
|
+
size fails to open with `0x8007000B`.
|
|
152
|
+
- **`Block/@Size` is the _compressed_ length** of each 64 KiB block, and is
|
|
153
|
+
omitted entirely for stored parts. Emitting it required deflating block by
|
|
154
|
+
block (`Z_FULL_FLUSH`), which `zipfile.writestr` cannot express — so the
|
|
155
|
+
archive is now written by hand.
|
|
156
|
+
- **An empty file has zero `<Block>` elements**, not one hashing `b""`.
|
|
157
|
+
- `inspect` wrongly expected `AppxMetadata/CodeIntegrity.cat` in the blockmap;
|
|
158
|
+
the signature covers it through `AXCI`.
|
|
159
|
+
|
|
160
|
+
### Fixed — false documentation
|
|
161
|
+
|
|
162
|
+
- `makemsix` cannot sign. `makemsix pack` accepts only `-d`/`-p`, and upstream
|
|
163
|
+
ships a signature _validator_, not a creator. The documented
|
|
164
|
+
`--cert`/`--cert-password` flow never worked and now fails with an explanation.
|
|
165
|
+
- The README quickstart did not run: the package lives under `src/`.
|
|
166
|
+
|
|
167
|
+
### Added — local checks for opaque device errors
|
|
168
|
+
|
|
169
|
+
Each of these was first seen as a hex code from a console:
|
|
170
|
+
|
|
171
|
+
- `Windows.FullTrustApplication` without the `runFullTrust` capability
|
|
172
|
+
(`0x80080204`, reported with only a line number).
|
|
173
|
+
- A certificate subject differing from `Identity/@Publisher` — refused before
|
|
174
|
+
signing instead of failing on the device.
|
|
175
|
+
- Malformed manifest XML (`0xC00CEE23`). `--` inside a comment is invalid XML and
|
|
176
|
+
easy to introduce while documenting a manifest.
|
|
177
|
+
- Real per-attribute checks for `Identity` `Name`/`Publisher`/`Version`,
|
|
178
|
+
replacing a check whose outer condition made it unreachable.
|
|
179
|
+
|
|
180
|
+
## 0.1.0
|
|
181
|
+
|
|
182
|
+
Initial skeleton: blockmap generation, pure-Python pack, layout validation,
|
|
183
|
+
minimal example, optional makemsix backend hook.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
7
|
+
pip install -e ".[dev,sign]"
|
|
8
|
+
pytest -q
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`[sign]` matters: without `cryptography`, the signature tests skip themselves
|
|
12
|
+
and the least forgiving code in the project goes unexercised.
|
|
13
|
+
|
|
14
|
+
## Before opening a pull request
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
ruff check src tests
|
|
18
|
+
mypy
|
|
19
|
+
coverage run -m pytest && coverage report # fails below 90%
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
CI runs exactly these on Python 3.10 through 3.14.
|
|
23
|
+
|
|
24
|
+
`ruff format` is **not** part of the contract, deliberately: a couple of places
|
|
25
|
+
here are laid out by hand because the automatic result reads worse — a `zip()`
|
|
26
|
+
over two long expressions, a CLI argument list written two-per-line. Match the
|
|
27
|
+
surrounding style; `ruff check` enforces what actually matters.
|
|
28
|
+
|
|
29
|
+
## How this codebase learns things
|
|
30
|
+
|
|
31
|
+
Nearly every serious defect here was found by comparing against something real —
|
|
32
|
+
a Microsoft-signed package, or a device accepting or rejecting an install — not
|
|
33
|
+
by reasoning about the specification. Two examples, both of which looked correct
|
|
34
|
+
and were not:
|
|
35
|
+
|
|
36
|
+
- `Block/@Size` is the _compressed_ length of a block, not the uncompressed one.
|
|
37
|
+
- Appx requires a ZIP64 end-of-central-directory but rejects ZIP64 extra fields
|
|
38
|
+
on the records. `zipfile` and `unzip` accept either.
|
|
39
|
+
|
|
40
|
+
So: **when a format detail is in doubt, check it against an artefact, and write
|
|
41
|
+
the check down as a test.** `tests/test_signature.py` fetches Microsoft's own
|
|
42
|
+
signed packages on demand for this purpose (`OPENAPPX_NO_NETWORK=1` skips them).
|
|
43
|
+
`docs/signing.md` records which install error corresponds to which mistake.
|
|
44
|
+
|
|
45
|
+
## Conventions
|
|
46
|
+
|
|
47
|
+
- The default pack path stays dependency-free. Anything needing a library goes
|
|
48
|
+
behind an optional extra, as signing does.
|
|
49
|
+
- `validate` runs before packing and must tolerate broken input; `inspect` runs
|
|
50
|
+
after and must not. Keep them apart.
|
|
51
|
+
- No branding, real application names, or third-party binaries in `examples/`.
|
|
52
|
+
- Never commit `.pfx`/`.p12` material. Public `.cer` files are fine.
|
openappx-0.6.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gianluca Mazza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# setuptools puts tests/test_*.py in the sdist on its own, but neither
|
|
2
|
+
# conftest.py nor anything the tests read. Without these, an sdist ships a suite
|
|
3
|
+
# that cannot run: every golden test errors on a missing fixture, test_pack.py
|
|
4
|
+
# packs examples/, and test_docs.py asserts against docs/ and CHANGELOG.md.
|
|
5
|
+
include tests/conftest.py
|
|
6
|
+
recursive-include examples *
|
|
7
|
+
recursive-include docs *.md
|
|
8
|
+
include CHANGELOG.md CONTRIBUTING.md SECURITY.md
|
openappx-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openappx
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Linux-first open-source Appx/MSIX layout validation and packing
|
|
5
|
+
Author-email: Gianluca Mazza <info@gianlucamazza.it>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gianlucamazza/openappx
|
|
8
|
+
Project-URL: Repository, https://github.com/gianlucamazza/openappx
|
|
9
|
+
Project-URL: Changelog, https://github.com/gianlucamazza/openappx/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Issues, https://github.com/gianlucamazza/openappx/issues
|
|
11
|
+
Keywords: msix,appx,packaging,windows,linux
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: POSIX
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
28
|
+
Requires-Dist: coverage>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
30
|
+
Provides-Extra: sign
|
|
31
|
+
Requires-Dist: cryptography>=43.0; extra == "sign"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# openappx
|
|
35
|
+
|
|
36
|
+
**Build, sign, bundle and install Windows app packages — from Linux, without Windows tooling.**
|
|
37
|
+
|
|
38
|
+
`openappx` replaces the `makeappx` and `signtool` half of a Windows app pipeline:
|
|
39
|
+
it turns a layout directory into a signed `.msix` or `.msixbundle`, checks it,
|
|
40
|
+
and installs it on a real device over the Windows Device Portal. Pure Python,
|
|
41
|
+
standard library only — signing is the one optional extra.
|
|
42
|
+
|
|
43
|
+
> **Status: beta.** The whole chain works and is verified on hardware:
|
|
44
|
+
> **pack → sign → deploy, from Linux, with no Windows tooling**. A real 47 MB
|
|
45
|
+
> UWP application, repackaged and signed by this project, installs on an Xbox
|
|
46
|
+
> One dev kit. Compiling PE/UWP binaries stays **out of scope** (see
|
|
47
|
+
> [Non-goals](#non-goals)) — [uwp-crossbuild](https://github.com/gianlucamazza/uwp-crossbuild)
|
|
48
|
+
> is the companion project that does that part.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install openappx # signing needs the extra: openappx[sign]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## What this is
|
|
57
|
+
|
|
58
|
+
| You have | openappx gives you |
|
|
59
|
+
| ------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
60
|
+
| A **layout directory** (manifest, assets, binaries, payload files) | A valid **`.msix` / Appx-style ZIP** with `AppxBlockMap.xml` and `[Content_Types].xml` |
|
|
61
|
+
| A code-signing certificate (or none — one can be minted) | A **signed** `.msix` that a real device installs |
|
|
62
|
+
| A signed `.msix` from anyone | Verification that the package matches the digests its signature covers |
|
|
63
|
+
| Packages for several architectures | An `.msixbundle` carrying them all |
|
|
64
|
+
| A device in developer mode | Installation over the Windows Device Portal, and the error if it is refused |
|
|
65
|
+
| CI on Linux | Deterministic pack tests without Windows |
|
|
66
|
+
|
|
67
|
+
It is **not** a replacement for MSBuild and the Windows SDK: it does not compile
|
|
68
|
+
anything. It replaces the packaging half — `makeappx`, `signtool`, and the
|
|
69
|
+
sideload step — of a Windows app pipeline.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Goals
|
|
74
|
+
|
|
75
|
+
- Pack an Appx/MSIX layout on Linux, macOS, or any host with Python 3.10+
|
|
76
|
+
- Generate a conformant `AppxBlockMap` (64 KiB SHA-256 blocks, compressed block
|
|
77
|
+
sizes) — checked against Microsoft-signed reference packages, not guessed
|
|
78
|
+
- Sign packages without Windows, and prove it by installing them on a device
|
|
79
|
+
- Validate common layout mistakes before pack (missing `AppxManifest.xml`, missing `Executable`, missing logos)
|
|
80
|
+
- Stay **dependency-light** (default path: Python standard library only)
|
|
81
|
+
- Produce **byte-reproducible** packages (fixed timestamps; same layout → same `.msix`)
|
|
82
|
+
- Remain **product-agnostic**: any app that ships as Appx/MSIX layout can use it
|
|
83
|
+
|
|
84
|
+
## Non-goals
|
|
85
|
+
|
|
86
|
+
| Non-goal | Why |
|
|
87
|
+
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
88
|
+
| Compiling Win32/UWP C++/C# into PE | A separate problem, with a separate tool: [uwp-crossbuild](https://github.com/gianlucamazza/uwp-crossbuild) |
|
|
89
|
+
| Emulating Windows or ReactOS as a build OS | Different problem domain |
|
|
90
|
+
| Guaranteeing Store certification | Store has additional policies beyond package shape |
|
|
91
|
+
| Replacing a device lab | `deploy` drives one device over Device Portal; orchestration is yours |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Architecture (host tools)
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
99
|
+
│ Your app build (elsewhere) │
|
|
100
|
+
│ produces: PE/DLLs + assets + AppxManifest.xml │
|
|
101
|
+
└────────────────────────────┬────────────────────────────────┘
|
|
102
|
+
│ layout directory
|
|
103
|
+
▼
|
|
104
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
105
|
+
│ openappx │
|
|
106
|
+
│ validate → blockmap → content types → zip (.msix) │
|
|
107
|
+
│ [optional] sign → AppxSignature.p7x → deploy to a device │
|
|
108
|
+
└────────────────────────────┬────────────────────────────────┘
|
|
109
|
+
│ .msix
|
|
110
|
+
▼
|
|
111
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
112
|
+
│ Install / sideload / store upload (your process) │
|
|
113
|
+
└─────────────────────────────────────────────────────────────┘
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
See [docs/architecture.md](docs/architecture.md) for the layers and extension
|
|
117
|
+
points, and [docs/format.md](docs/format.md) for the container and blockmap rules
|
|
118
|
+
— each recorded with the measurement that established it, since several are not
|
|
119
|
+
what the specification suggests.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Quickstart
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# From repo root (no install required)
|
|
127
|
+
./scripts/pack.sh --root examples/minimal-layout --out /tmp/example.msix
|
|
128
|
+
|
|
129
|
+
unzip -l /tmp/example.msix | head
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`scripts/pack.sh` is a thin wrapper that puts `src/` on `PYTHONPATH`. The equivalent
|
|
133
|
+
without the wrapper:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
PYTHONPATH=src python3 -m openappx.pack --root examples/minimal-layout --out /tmp/example.msix
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
With an editable install:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
143
|
+
pip install -e ".[dev]"
|
|
144
|
+
openappx pack --root examples/minimal-layout --out /tmp/example.msix
|
|
145
|
+
pytest -q
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Layout contract
|
|
149
|
+
|
|
150
|
+
A pack root must contain at least:
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
AppxManifest.xml
|
|
154
|
+
…payload files referenced by the manifest (exe, logos, etc.)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
openappx **writes** (do not pre-seed):
|
|
158
|
+
|
|
159
|
+
- `[Content_Types].xml`
|
|
160
|
+
- `AppxBlockMap.xml`
|
|
161
|
+
- `AppxSignature.p7x` — written by `openappx sign`, never by `pack`
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## CLI
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
openappx pack --root DIR --out FILE.msix [options]
|
|
169
|
+
|
|
170
|
+
--backend python|makemsix default: python (unsigned)
|
|
171
|
+
--makemsix PATH makemsix binary (default: tools/bin/makemsix)
|
|
172
|
+
--allow-missing pack even if validation reports gaps
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
openappx sign --package FILE.msix --pfx CERT.pfx
|
|
177
|
+
openappx sign --make-test-cert "CN=Publisher" --cert-out mycert
|
|
178
|
+
openappx validate --root DIR # check a layout before packing
|
|
179
|
+
openappx unpack --package FILE.msix --out DIR
|
|
180
|
+
openappx inspect --package FILE.msix [--json]
|
|
181
|
+
openappx deploy --device URL --user NAME --package FILE.msix [--insecure]
|
|
182
|
+
openappx bundle --package A.msix --package B.msix --out X.msixbundle
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
A bundle carries one application across architectures, plus any resource
|
|
186
|
+
packages. **Sign the packages first, then the bundle** — signing only the
|
|
187
|
+
container is rejected as if the container were unsigned:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
openappx sign --package app-x64.msix --pfx cert.pfx
|
|
191
|
+
openappx sign --package app-x86.msix --pfx cert.pfx
|
|
192
|
+
openappx bundle --package app-x64.msix --package app-x86.msix --out app.msixbundle
|
|
193
|
+
openappx sign --package app.msixbundle --pfx cert.pfx
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`inspect` is the read side of `pack`: it re-derives every block hash from the bytes
|
|
197
|
+
stored in the archive and compares them with `AppxBlockMap.xml`, checks `LfhSize`
|
|
198
|
+
against the ZIP local headers actually written, and verifies that `[Content_Types].xml`
|
|
199
|
+
covers every part. On a signed package it also recomputes the digests the signature
|
|
200
|
+
covers and reports any mismatch — see [docs/signing.md](docs/signing.md). It works on
|
|
201
|
+
packages produced by any tool, not just openappx.
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
Package: /tmp/example.msix (3139 bytes)
|
|
205
|
+
Identity: Name=OpenAppx.Example Publisher=CN=OpenAppx-Example Version=0.1.0.0 ProcessorArchitecture=x64
|
|
206
|
+
Signature: absent
|
|
207
|
+
|
|
208
|
+
Part Size Stored Method Blocks
|
|
209
|
+
app.exe 31 31 store 1
|
|
210
|
+
AppxManifest.xml 1659 737 deflate 1
|
|
211
|
+
Assets/StoreLogo.png 67 66 deflate 1
|
|
212
|
+
[Content_Types].xml 1061 1061 store -
|
|
213
|
+
AppxBlockMap.xml 610 610 store -
|
|
214
|
+
|
|
215
|
+
OK: blockmap and content types are consistent with the archive
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Deploying to a device
|
|
219
|
+
|
|
220
|
+
`deploy` talks to the [Windows Device Portal](https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/device-portal),
|
|
221
|
+
the same REST service on Xbox, HoloLens, IoT and Windows desktop — so a real
|
|
222
|
+
device tells you whether a package is actually installable:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
export OPENAPPX_DEVICE_PASSWORD='…' # keeps it out of `ps`
|
|
226
|
+
openappx deploy --device https://192.168.1.50:11443 --user devuser \
|
|
227
|
+
--package example.msix --insecure
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- The device must be in **Developer Mode** with Device Portal enabled
|
|
231
|
+
(on Xbox: Dev Home → Home → Remote Access → Remote Access Settings).
|
|
232
|
+
- `--insecure` is required because devices serve a self-signed certificate.
|
|
233
|
+
- CSRF is handled by the cookie-to-header handshake the Device Portal UI uses
|
|
234
|
+
(`CSRF-Token` cookie → `X-CSRF-Token` header). `--csrf-bypass` switches to
|
|
235
|
+
Microsoft's `auto-<username>` escape hatch instead; that account must then
|
|
236
|
+
never be used in the web UI.
|
|
237
|
+
- `--list` shows installed packages, `--uninstall PACKAGE_FULL_NAME` removes one,
|
|
238
|
+
`--install-cert CERT.cer` trusts a certificate on the device.
|
|
239
|
+
|
|
240
|
+
**Sideloading requires a signed package** and a certificate the device trusts.
|
|
241
|
+
The full loop, entirely from Linux:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
openappx sign --make-test-cert "CN=OpenAppx-Example" --cert-out mycert
|
|
245
|
+
openappx deploy --device https://<ip>:11443 --user NAME --install-cert mycert.cer
|
|
246
|
+
openappx pack --root examples/resource-only --out app.msix
|
|
247
|
+
openappx sign --package app.msix --pfx mycert.pfx --timestamp
|
|
248
|
+
openappx deploy --device https://<ip>:11443 --user NAME --package app.msix
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`examples/resource-only/` is the layout this loop was verified with: it installs
|
|
252
|
+
on an Xbox One dev kit. `examples/minimal-layout/` shows a desktop full-trust
|
|
253
|
+
manifest instead, and deliberately ships a placeholder executable, so it packs
|
|
254
|
+
and signs but stops at the deployment stage.
|
|
255
|
+
|
|
256
|
+
Signing needs `pip install 'openappx[sign]'`; everything else is stdlib-only.
|
|
257
|
+
[docs/signing.md](docs/signing.md) has the format details and the console
|
|
258
|
+
responses that verify each step.
|
|
259
|
+
|
|
260
|
+
Exit codes: `0` success, `1` failure (pack error, a failed deploy, or problems
|
|
261
|
+
found by `validate` / `inspect`), `2` bad usage or an unreadable input.
|
|
262
|
+
|
|
263
|
+
Without an editable install, replace `openappx pack` with
|
|
264
|
+
`PYTHONPATH=src python3 -m openappx.pack` (same for `validate` and `inspect`).
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Optional: makemsix backend
|
|
269
|
+
|
|
270
|
+
Microsoft’s open-source MSIX SDK can pack on Linux when built with pack support:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
./scripts/bootstrap-makemsix.sh # may fail on bleeding-edge toolchains
|
|
274
|
+
openappx pack --backend makemsix --root … --out …
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
It produces **unsigned** packages, exactly like the Python backend: `makemsix pack`
|
|
278
|
+
takes only `-d`/`-p`, and upstream implements signature _validation_, not creation.
|
|
279
|
+
See [docs/signing.md](docs/signing.md). The pure-Python backend remains the default
|
|
280
|
+
and is the one covered by the test suite.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Project layout
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
openappx/
|
|
288
|
+
├── src/openappx/
|
|
289
|
+
│ ├── cli.py # the `openappx` entry point; dispatches lazily
|
|
290
|
+
│ ├── blockmap.py # block hashing, XML rendering, ZIP header parsing
|
|
291
|
+
│ ├── pack_core.py # the ZIP writer and the two pack backends
|
|
292
|
+
│ ├── pack.py # pack CLI
|
|
293
|
+
│ ├── bundle.py # .msixbundle assembly
|
|
294
|
+
│ ├── validate.py # pre-pack layout checks
|
|
295
|
+
│ ├── inspect.py # post-pack package and bundle checks
|
|
296
|
+
│ ├── unpack.py # extract a layout back out of a package
|
|
297
|
+
│ ├── deploy.py # Windows Device Portal client
|
|
298
|
+
│ └── sign/ # digests, DER encoder, signature creation
|
|
299
|
+
├── docs/
|
|
300
|
+
│ ├── architecture.md # the layers and where to extend them
|
|
301
|
+
│ ├── format.md # container and blockmap rules, with their evidence
|
|
302
|
+
│ ├── signing.md # what AppxSignature.p7x contains, decoded
|
|
303
|
+
│ └── roadmap.md # done, not done, and why
|
|
304
|
+
├── examples/
|
|
305
|
+
│ ├── minimal-layout/ # desktop, full-trust; placeholder exe, so it never installs
|
|
306
|
+
│ └── resource-only/ # installs on a device — used to prove the chain
|
|
307
|
+
├── tests/
|
|
308
|
+
└── scripts/
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Known limits
|
|
314
|
+
|
|
315
|
+
| Limit | Detail |
|
|
316
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
317
|
+
| 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](docs/format.md). `pack` fails with that explanation. |
|
|
318
|
+
| Memory | `pack` builds the archive in memory: fine for tens of MB, not for GB. |
|
|
319
|
+
| 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. |
|
|
320
|
+
| Timestamping | `--timestamp` implemented; that Windows honours it past certificate expiry is untestable here. |
|
|
321
|
+
| Bundles | A bundle mixing an application and a language pack registers only if both `resources.pri` merge; ours do not yet (`0x80070002`). |
|
|
322
|
+
| CodeIntegrity | `AppxMetadata/CodeIntegrity.cat` is verified when present, never generated. It matters only where Device Guard is enforced. |
|
|
323
|
+
| Certificate trust | `inspect` reports the signer and checks publisher agreement and expiry, but never the chain of trust. |
|
|
324
|
+
|
|
325
|
+
## Roadmap
|
|
326
|
+
|
|
327
|
+
Pack, sign, bundle and deploy are done and verified on hardware. What remains is
|
|
328
|
+
in [docs/roadmap.md](docs/roadmap.md), with the reason for each: streaming pack,
|
|
329
|
+
`CodeIntegrity.cat`, and merged resource bundles.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Contributing
|
|
334
|
+
|
|
335
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and the checks CI runs, and
|
|
336
|
+
[SECURITY.md](SECURITY.md) for how keys, device credentials and untrusted
|
|
337
|
+
archives are handled.
|
|
338
|
+
|
|
339
|
+
The short version: keep the default pack path dependency-free, and when a format
|
|
340
|
+
detail is in doubt, verify it against a real package or a real device rather than
|
|
341
|
+
against the specification — that is how every serious bug here was found.
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT — see [LICENSE](LICENSE).
|