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.
Files changed (57) hide show
  1. openappx-0.6.0/CHANGELOG.md +183 -0
  2. openappx-0.6.0/CONTRIBUTING.md +52 -0
  3. openappx-0.6.0/LICENSE +21 -0
  4. openappx-0.6.0/MANIFEST.in +8 -0
  5. openappx-0.6.0/PKG-INFO +345 -0
  6. openappx-0.6.0/README.md +312 -0
  7. openappx-0.6.0/SECURITY.md +38 -0
  8. openappx-0.6.0/docs/architecture.md +64 -0
  9. openappx-0.6.0/docs/format.md +176 -0
  10. openappx-0.6.0/docs/roadmap.md +142 -0
  11. openappx-0.6.0/docs/signing.md +203 -0
  12. openappx-0.6.0/examples/minimal-layout/AppxManifest.xml +47 -0
  13. openappx-0.6.0/examples/minimal-layout/Assets/StoreLogo.png +0 -0
  14. openappx-0.6.0/examples/minimal-layout/app.exe +1 -0
  15. openappx-0.6.0/examples/resource-only/AppxManifest.xml +36 -0
  16. openappx-0.6.0/examples/resource-only/Assets/StoreLogo.png +0 -0
  17. openappx-0.6.0/pyproject.toml +67 -0
  18. openappx-0.6.0/setup.cfg +4 -0
  19. openappx-0.6.0/src/openappx/__init__.py +3 -0
  20. openappx-0.6.0/src/openappx/__main__.py +3 -0
  21. openappx-0.6.0/src/openappx/blockmap.py +240 -0
  22. openappx-0.6.0/src/openappx/bundle.py +346 -0
  23. openappx-0.6.0/src/openappx/cli.py +56 -0
  24. openappx-0.6.0/src/openappx/deploy.py +482 -0
  25. openappx-0.6.0/src/openappx/inspect.py +506 -0
  26. openappx-0.6.0/src/openappx/pack.py +91 -0
  27. openappx-0.6.0/src/openappx/pack_core.py +358 -0
  28. openappx-0.6.0/src/openappx/sign/__init__.py +40 -0
  29. openappx-0.6.0/src/openappx/sign/asn1.py +112 -0
  30. openappx-0.6.0/src/openappx/sign/certificate.py +116 -0
  31. openappx-0.6.0/src/openappx/sign/cli.py +118 -0
  32. openappx-0.6.0/src/openappx/sign/digest.py +173 -0
  33. openappx-0.6.0/src/openappx/sign/signer.py +363 -0
  34. openappx-0.6.0/src/openappx/sign/timestamp.py +127 -0
  35. openappx-0.6.0/src/openappx/unpack.py +90 -0
  36. openappx-0.6.0/src/openappx/validate.py +169 -0
  37. openappx-0.6.0/src/openappx.egg-info/PKG-INFO +345 -0
  38. openappx-0.6.0/src/openappx.egg-info/SOURCES.txt +55 -0
  39. openappx-0.6.0/src/openappx.egg-info/dependency_links.txt +1 -0
  40. openappx-0.6.0/src/openappx.egg-info/entry_points.txt +2 -0
  41. openappx-0.6.0/src/openappx.egg-info/requires.txt +9 -0
  42. openappx-0.6.0/src/openappx.egg-info/top_level.txt +1 -0
  43. openappx-0.6.0/tests/conftest.py +90 -0
  44. openappx-0.6.0/tests/test_bundle.py +348 -0
  45. openappx-0.6.0/tests/test_certificate_timestamp.py +210 -0
  46. openappx-0.6.0/tests/test_cli.py +128 -0
  47. openappx-0.6.0/tests/test_deploy.py +403 -0
  48. openappx-0.6.0/tests/test_docs.py +112 -0
  49. openappx-0.6.0/tests/test_format.py +211 -0
  50. openappx-0.6.0/tests/test_inspect.py +219 -0
  51. openappx-0.6.0/tests/test_makemsix_backend.py +149 -0
  52. openappx-0.6.0/tests/test_pack.py +177 -0
  53. openappx-0.6.0/tests/test_sign_cli.py +129 -0
  54. openappx-0.6.0/tests/test_signature.py +220 -0
  55. openappx-0.6.0/tests/test_signer.py +220 -0
  56. openappx-0.6.0/tests/test_unpack.py +131 -0
  57. 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
@@ -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).