TASK 48 slice 2: the app packs as a Microsoft Store MSIX

The manifest the Store route needs, and a real package built from it.

Packaging/Package.appxmanifest is a full-trust packagedClassicApp: EntryPoint
Windows.FullTrustApplication, uap10:TrustLevel=mediumIL,
uap10:RuntimeBehavior=packagedClassicApp, the rescap runFullTrust capability, and
webcam + microphone DeviceCapabilities. That combination is what lets the packaged
app launch the bundled ffmpeg as a child process and write to the user's profile;
an appContainer package would virtualise the recordings folder instead. x64 only,
en-US only (policy 10.7).

Packaging/Assets/ carries the four tiles the packer requires, generated from the
app's own Assets/llamacasty-logo.jpg by scripts/make-store-logos.py - listing art
and app art cannot drift.

scripts/package-msix.sh publishes Release win-x64 self-contained into
artifacts/msix-stage (a RID-less publish would copy every RID's ONNX native),
stages the manifest as AppxManifest.xml + Assets, and runs the Windows SDK's
makeappx. MSIX_PFX/MSIX_PFX_PASSWORD sign a package for a local test install; the
Store signs submissions. artifacts/ is gitignored.

First real package: artifacts/LlamaCasty.msix, 159 MB, 509 files, 'Package
creation succeeded'.

Schema traps found by packing (makeappx reports one error at a time):
Square310x310Logo and Wide310x150Logo are NOT uap:VisualElements attributes - they
live on uap:DefaultTile.

Guard: StorePackagingTests.The_package_manifest_declares_full_trust_camera_and_
microphone - identity/architecture, the full-trust application element, the three
capabilities, no appContainer VALUE (not the file text: the manifest's own comment
says the word), en-US only, and every referenced logo present.

Full suite 438/438, 0 warnings. Cold-start smoke test after the Velopack removal:
MainWindow loaded, no exceptions.
This commit is contained in:
2026-10-22 09:05:50 -07:00
parent 20cfadca0c
commit daac7689fd
16 changed files with 412 additions and 26 deletions
+4
View File
@@ -10,6 +10,10 @@ Helpers/OAuthCredentials.cs
.vs/
*.DotSettings.user
# Build artifacts — the MSIX + its staging tree are produced by scripts/package-msix.sh,
# never committed (the package embeds the published app and the bundled ffmpeg).
artifacts/
# Confidential business files (gitignored, not committed)
MONETIZATION.md
MARCOM.md
+11 -9
View File
@@ -305,18 +305,20 @@ steps:
### 4.1 Files to Ship
```
LlamaCasty.exe (single-file, self-contained, ~80-120MB)
LlamaCasty.msix (full-trust x64 package, self-contained, ~159MB)
```
Single-file means everything is inside the `.exe`. No DLLs, no runtime dependency.
Built by **`scripts/package-msix.sh`** (TASK 48 item 2, shipped 2026-10-22): it publishes
`Release -r win-x64 --self-contained` into `artifacts/msix-stage`, stages
`Packaging/Package.appxmanifest` (as `AppxManifest.xml`) + `Packaging/Assets/`, and packs with
the Windows SDK's `makeappx` → `artifacts/LlamaCasty.msix` (gitignored). Unsigned by default —
**the Store signs submissions**; set `MSIX_PFX`/`MSIX_PFX_PASSWORD` to sign a package for a
local test install. The manifest is a full-trust `packagedClassicApp` at `mediumIL` with
`runFullTrust` + webcam + microphone (see `TASKS/task-48-distribution-msix.md` item 2).
**Why not an installer?**
- Single `.exe` is simpler to distribute and update.
- No admin rights required (installs to `%LOCALAPPDATA%`).
- No installer framework to maintain (WiX, Inno Setup, etc.).
- User drops it anywhere and runs it.
**Caveat:** Self-contained single-file means ~80-120MB due to bundled .NET runtime. For smaller size, use framework-dependent publishing (requires .NET 8 runtime on user machines) — drops to ~5-10MB.
A single-file `.exe` is **no longer the artifact**: the Store needs a package, and the package
carries the runtime (~80-120MB of it) plus the bundled ffmpeg (~135MB of DLLs). No installer
framework and no admin rights — the Store handles install, update and uninstall.
### 4.2 Distribution via Polar — ⛔ OBSOLETE (2026-09-27)
+15 -4
View File
@@ -77,6 +77,17 @@
unchanged; the seams are two `internal` properties). Guard:
`ytLive.Tests/AppLogRotationTests.cs`. Slices 2 (temp alert-clip cleanup) and 3 (low-disk
guard before recording) are still open.
- **TASK 48 slice 2 — the app PACKS as a Store MSIX (shipped 2026-10-22).** `Packaging/Package.appxmanifest`
(full trust: `Windows.FullTrustApplication` + `uap10:TrustLevel=mediumIL` +
`uap10:RuntimeBehavior=packagedClassicApp` + `rescap runFullTrust`, plus webcam/microphone
`DeviceCapability`s, x64, en-US only) + `Packaging/Assets/` (four logo tiles generated from the
app's own artwork by `scripts/make-store-logos.py`) + `scripts/package-msix.sh` (publish
`Release -r win-x64 --self-contained` → stage → `makeappx pack`, optional `MSIX_PFX` signing).
**First real package built: `artifacts/LlamaCasty.msix`, 159 MB, 509 files, `Package creation
succeeded`** (`artifacts/` is gitignored). Guard:
`StorePackagingTests.The_package_manifest_declares_full_trust_camera_and_microphone`.
⚠️ Before submission the manifest's `Identity/@Publisher` and `PublisherDisplayName` must be
set to the exact values Partner Center shows — a mismatch is a rejection.
- **TASK 48 slice 3 — the self-updater is gone (shipped 2026-10-22).** The Store route (creator
ruling 2026-09-27) means the Store updates the app, so the Velopack package + bootstrap were
**deleted** (`ytLive.csproj`, `App.xaml.cs`) — a compliance action, not cleanup: the updater
@@ -280,10 +291,10 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel
Galaxy `cef.log` at 2.7 GB is NOT ours.)
## Tests
- Last full run: **437/437 passed, 0 warnings** (2026-10-22 — after TASK 48 slice 3's
`StorePackagingTests`; the run before was 436/436 after TASK 55 slice 1's
`AppLogRotationTests`, and the one before that 435/435 after TASK 63). The one failure seen
earlier in the
- Last full run: **438/438 passed, 0 warnings** (2026-10-22 — after TASK 48 slice 2's
manifest guard; the run before was 437/437 after slice 3's `StorePackagingTests`, then
436/436 after TASK 55 slice 1's `AppLogRotationTests` and 435/435 after TASK 63). The one
failure seen earlier in the
session was `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
which **passes in isolation** (re-run: 3/3) — the known real-mouse flake, not a regression.
- Known flaky-by-environment (all pass in isolation): `AudioPipelineTests.Mix_HonorsProviderGains…`,
+41
View File
@@ -2320,3 +2320,44 @@ and change the callers. Don't.
temp directory. Never let the test write to the real `%APPDATA%` log.
4. **Never throw from the rotation**: a failed rotation must degrade to the old behaviour
(the log keeps growing), never take the app down. Same rule as the logger itself.
## 2026-10-22 — PACK A WPF DESKTOP APP AS A MICROSOFT STORE MSIX (RECIPE)
No installer project, no WiX, no `.wapproj`: `dotnet publish` + the Windows SDK's `makeappx`
is enough, and it is what `scripts/package-msix.sh` does.
**The pipeline:**
1. `dotnet publish -c Release -r win-x64 --self-contained -o artifacts/msix-stage` — the RID
matters: an RID-less publish copies **every** RID's native folder (ONNX Runtime, ~17 MB
each) and the package bloats. Self-contained so the package carries its own .NET runtime.
2. The manifest goes at the **package root as `AppxManifest.xml`** (not `Package.appxmanifest`)
with its image folder beside it (`Assets\...` paths are relative to the root).
3. `makeappx pack /d <stage> /p <out>.msix /o` → `Package creation succeeded`. Signing is the
Store's job; `signtool sign /fd SHA256 /f dev.pfx` is only for a local test install.
4. Find the tools with a glob, not a hard-coded SDK version:
`ls -d ".../Windows Kits/10/bin/"*/x64/makeappx.exe | sort -V | tail -1`.
**Full trust, for a real Win32 app (this is the part that decides whether the app still works):**
```xml
<Application Executable="app.exe" EntryPoint="Windows.FullTrustApplication"
uap10:TrustLevel="mediumIL" uap10:RuntimeBehavior="packagedClassicApp">
```
plus `<rescap:Capability Name="runFullTrust" />` and `DeviceCapability` entries for
`webcam`/`microphone`. `packagedClassicApp` is what lets the packaged exe launch a **child
process** (a bundled ffmpeg); an `appContainer` package silently virtualises file writes
instead — recordings "vanish".
**The schema traps — makeappx validates ONE error at a time, so budget 2–3 pack runs:**
- `Square310x310Logo` and `Wide310x150Logo` are **not** attributes of `uap:VisualElements`;
they belong on `<uap:DefaultTile>`, a child of `VisualElements`. (`Square44x44Logo` /
`Square150x150Logo` / `BackgroundColor` *are* on `VisualElements`.)
- Every referenced image must exist at the stated size (`StoreLogo` 50×50, 44×44, 150×150,
310×310, 310×150) — the packer fails on a missing file, and the Store rejects a wrong size.
- Localization: `<Resource Language="en-US"/>` only, unless the listing description is
translated into every declared language.
**Testing the manifest without a Store account:** a source-level xunit guard over the XML —
identity/architecture, the full-trust application attributes, the capabilities, en-US only, and
"every referenced logo exists". ⚠️ Check `appContainer` absence on the **attribute values**,
not `manifest.ToString()` — the file's own comment is *supposed* to say "not appContainer", and
a text-wide ban fails on it (same trap as the `force-ssl`/`Download` guards).
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+78
View File
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
LlamaCasty — Microsoft Store MSIX manifest (TASK 48).
Full trust is the whole point of this file: the app records to the user's profile,
launches the bundled ffmpeg as a CHILD PROCESS, opens an ordinary Win32 folder picker,
and drives WebView2. None of that works in an AppContainer, so this is a
packagedClassicApp at mediumIL with the runFullTrust restricted capability.
⛔ Do NOT change this to appContainer: recording silently starts writing to a
per-package virtualised folder and the output "vanishes" (TASK 48 item 6).
⚠️ Before submission, replace Publisher with the exact Publisher value Partner Center
shows for the account, and PublisherDisplayName with its display name — a mismatch is
a certification rejection. The version is bumped per submission.
-->
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap uap10 rescap">
<Identity
Name="LlamaCasty"
Publisher="CN=LlamaCasty"
Version="1.0.0.0"
ProcessorArchitecture="x64" />
<Properties>
<DisplayName>LlamaCasty</DisplayName>
<PublisherDisplayName>LlamaCasty</PublisherDisplayName>
<Description>Live streaming to YouTube: scenes, sources, a webcam with background effects, alerts, chat and recording.</Description>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily
Name="Windows.Desktop"
MinVersion="10.0.19041.0"
MaxVersionTested="10.0.26100.0" />
</Dependencies>
<!-- English only (Store policy 10.7): a second language here means the listing
description must be localised into it too. -->
<Resources>
<Resource Language="en-US" />
</Resources>
<Applications>
<Application
Id="LlamaCasty"
Executable="ytLive.exe"
EntryPoint="Windows.FullTrustApplication"
uap10:TrustLevel="mediumIL"
uap10:RuntimeBehavior="packagedClassicApp">
<uap:VisualElements
DisplayName="LlamaCasty"
Description="Live streaming to YouTube: scenes, sources, a webcam with background effects, alerts, chat and recording."
BackgroundColor="#171928"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<!-- The wide and 310 tiles live on DefaultTile, not on VisualElements — the
Windows 10 uap:VisualElements schema has no such attributes. -->
<uap:DefaultTile
Wide310x150Logo="Assets\Wide310x150Logo.png"
Square310x310Logo="Assets\Square310x310Logo.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<DeviceCapability Name="webcam" />
<DeviceCapability Name="microphone" />
</Capabilities>
</Package>
+1 -1
View File
@@ -63,7 +63,7 @@
| 45 | TEST-tab chat fix #2: insert body must declare `snippet.type` (400 MISSING_REQUIRED_FIELD) | ✅ Done (2026-09-25) | [`TASKS/task-45-chat-insert-type.md`](TASKS/task-45-chat-insert-type.md) |
| 46 | Drawers: click outside the rail closes whichever is open — TEST added to the existing Stream Settings + YPP dismiss behavior | ✅ Done (2026-09-25) | [`TASKS/task-46-drawer-click-outside-close.md`](TASKS/task-46-drawer-click-outside-close.md) |
| 47 | Alert box video: built-in/custom alert clip (+ six-animation fallback) with read-time fade in/out + ticker (now in the preview too, 3 display methods) + alert volume in the live mix | ✅ Done (2026-09-26) | [`TASKS/task-47-alert-videos.md`](TASKS/task-47-alert-videos.md) |
| 48 | Distribution & packaging: **MSIX + Store IAP** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28**; **item 3 (remove the Velopack updater) SHIPPED 2026-10-22** — the Store updates the app | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) |
| 48 | Distribution & packaging: **MSIX + Store IAP** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28**; **item 2 (`Package.appxmanifest` + `scripts/package-msix.sh`) SHIPPED 2026-10-22 — a real 159 MB `LlamaCasty.msix` packs**; **item 3 (remove the Velopack updater) SHIPPED 2026-10-22** — the Store updates the app. Open: items 6-8 (recording invariant, disqualifiers, pre-submission gates); items 4/5 are the creator's (Certification Kit, Partner Center listing) | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) |
| 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) |
| 50 | One-button "Publish this video" (resumable upload of the local recording) | ☐ Queued (2026-09-28) | [`TASKS/task-50-publish-recording.md`](TASKS/task-50-publish-recording.md) |
| 51 | **Webcam background effects: Remove + Blur** | ✅ **Built 2026-10-09** (v1 requirement) — bundled MediaPipe Selfie Segmenter → ONNX (verified vs Google's runtime), Remove + Blur with strength, per-scene config persisted, one processing point in CameraManager, radio-pill UI in the webcam's Layers properties. Creator check 2026-10-09: **Remove jitter FIXED** (temporal mask smoothing 0.75/0.25, OBS/Jitsi-derived) and **Blur "heavy" FIXED** (quadratic radius 1→6 small px, default 0.5 = 2, StreamFX-consensus); recheck on the real camera pending | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) |
+32 -12
View File
@@ -140,24 +140,44 @@ executes** — its teardown (item 3) is a compliance action, not just cleanup.
---
## 2. ☐ `Package.appxmanifest` — full trust, camera, microphone
## 2. ✅ `Package.appxmanifest` — full trust, camera, microphone (DONE 2026-10-22)
There is currently **no `.manifest` file in the repo at all**, so this is purely additive.
There was **no `.manifest` file in the repo at all**, so this was purely additive.
**Shipped:** `Packaging/Package.appxmanifest` + `Packaging/Assets/` (the four logo tiles) +
`scripts/package-msix.sh` (publish → stage → `makeappx pack`, optional `signtool` via
`MSIX_PFX`). The first real package was built on 2026-10-22: **`Package creation succeeded`,
159 MB, 509 files** in `artifacts/LlamaCasty.msix` (gitignored). The logos are generated from
the app's own artwork by `scripts/make-store-logos.py`, so listing art and app art cannot drift.
- `rescap:Capability Name="runFullTrust"` — medium integrity, not AppContainer
- `webcam` and `microphone` capabilities
- `uap10:TrustLevel="mediumIL"` and `uap10:RuntimeBehavior="packagedClassicApp"` — the
latter is what allows launching package executables as child processes
- Declare **English only** (10.7 — otherwise the description must be localized into every
declared language)
- Block map SHA2-256, `StoreManifest`, under the 25 GB cap
- **x64 only** (64-bit requirement, creator ruling 2026-10-09 — `PlatformTarget=x64` already in
both csproj files): package architecture `x64`, publish with `-r win-x64` so only the win-x64
native folders ship (an RID-less build copies every RID's ONNX Runtime native, ~17 MB each).
Store listing system requirements: "64-bit Windows 10 or 11".
- `DeviceCapability` **webcam** and **microphone**
- `uap10:TrustLevel="mediumIL"` and `uap10:RuntimeBehavior="packagedClassicApp"` on
`<Application>` — the latter is what allows launching package executables (the bundled
ffmpeg) as child processes
- **English only** (`<Resource Language="en-US"/>`) — 10.7: otherwise the description must be
localized into every declared language
- Block map SHA2-256 is generated by `makeappx`; no `StoreManifest` is needed for a plain
MSIX submission; 159 MB is far under the 25 GB cap
- **x64 only** (creator ruling 2026-10-09 — `PlatformTarget=x64` already in both csproj files):
`ProcessorArchitecture="x64"`, published `-r win-x64 --self-contained` so only the win-x64
native folders ship (an RID-less build copies every RID's ONNX Runtime native, ~17 MB each)
and the package carries its own .NET runtime. Store listing system requirements:
"64-bit Windows 10 or 11".
**Do NOT use `appContainer`** — see the invariant in item 6.
**Schema gotchas found the hard way** (the packer rejects the manifest, one error at a time):
`Square310x310Logo` and `Wide310x150Logo` are **NOT** attributes of `uap:VisualElements` in the
Windows 10 schema — they belong on `<uap:DefaultTile>`, a child of `VisualElements`. And
`Assert.DoesNotContain("appContainer", manifest.ToString())` fails on the manifest's own
comment: guard the **attribute values**, not the file text.
**Guard:** `ytLive.Tests/StorePackagingTests.The_package_manifest_declares_full_trust_camera_and_microphone`
— identity (x64), full-trust application element, the three capabilities, no `appContainer`
value, en-US only, and every referenced logo exists in `Packaging/` (makeappx refuses to pack
otherwise).
## 3. ✅ Remove Velopack — 3 edit sites (DONE 2026-10-22)
The Store handles updates, so this is simply **deleted**. Trivially removable; the code was
+9
View File
@@ -2587,6 +2587,15 @@ before re-deriving it. Executable checklist: `TASKS/task-48-distribution-msix.md
(TASK 48 slice 3): the `PackageReference` in `ytLive.csproj` and the bootstrap in
`App.xaml.cs` are gone, and `ytLive.Tests/StorePackagingTests.cs` guards that no updater
package or updater API comes back. The self-hosted DO droplet dies with it.
- **The package is built by `scripts/package-msix.sh` (2026-10-22, TASK 48 slice 2)** — publish
`Release -r win-x64 --self-contained` into `artifacts/msix-stage`, copy
`Packaging/Package.appxmanifest` in as `AppxManifest.xml` + `Packaging/Assets/`, then
`makeappx pack` → `artifacts/LlamaCasty.msix` (159 MB, gitignored; `MSIX_PFX` signs it for a
local install). The manifest is a **full-trust `packagedClassicApp` at `mediumIL`** with
`runFullTrust` + webcam + microphone — that is what lets the app launch the bundled ffmpeg as
a child process; an `appContainer` package would virtualise the recordings folder instead.
`Square310x310Logo`/`Wide310x150Logo` live on `uap:DefaultTile`, **not** on `VisualElements`.
Logos come from the app's own artwork via `scripts/make-store-logos.py`.
- **⛔ The Polar licensing subsystem is deleted** — `PolarLicenseService`, `PolarLicense`,
`MainViewModel.License.cs` (`PremiumUrl`, customer portal, the
`OfflineGracePeriod = 14 days` subscription-era artifact, renewal/lapse copy), and the
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Generate the Store logo tiles from the app's own brand artwork.
The MSIX manifest references four images that must exist inside the package
(50x50 StoreLogo, 44x44, 150x150, 310x150 wide). They are derived from
`Assets/llamacasty-logo.jpg` — the same artwork the app shows in the overlay —
so the listing art and the app art cannot drift apart.
The bright mark is located by luminance (the artwork is a dark navy field with a
light mark in the middle), cropped, and re-laid on the artwork's own background
colour so the tiles look like the app.
Run: python3 scripts/make-store-logos.py
"""
import os
from PIL import Image
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SOURCE = os.path.join(ROOT, "Assets", "llamacasty-logo.jpg")
OUT_DIR = os.path.join(ROOT, "Packaging", "Assets")
def background(image: Image.Image) -> tuple:
"""The artwork's own background colour (a corner pixel)."""
return image.getpixel((4, 4))
def mark_crop(image: Image.Image) -> tuple:
"""The bbox of the light mark, found by luminance threshold."""
grey = image.convert("L")
bright = grey.point(lambda p: 255 if p > 190 else 0)
bbox = bright.getbbox()
if bbox is None:
raise SystemExit("no bright mark found in the source artwork")
return bbox
def tile(image: Image.Image, crop: tuple, size: int) -> Image.Image:
"""A square tile: the mark scaled to fit, on the artwork's background."""
canvas = Image.new("RGB", (size, size), background(image))
mark = image.crop(crop)
scale = size / max(mark.size)
mark = mark.resize((max(1, int(mark.width * scale)), max(1, int(mark.height * scale))))
canvas.paste(mark, ((size - mark.width) // 2, (size - mark.height) // 2))
return canvas
def wide(image: Image.Image, crop: tuple, width: int, height: int) -> Image.Image:
"""A wide tile: the mark at the artwork's height, centred on the background."""
canvas = Image.new("RGB", (width, height), background(image))
mark = image.crop(crop)
scale = height / mark.height
mark = mark.resize((max(1, int(mark.width * scale)), height))
canvas.paste(mark, ((width - mark.width) // 2, 0))
return canvas
def main() -> None:
os.makedirs(OUT_DIR, exist_ok=True)
image = Image.open(SOURCE).convert("RGB")
crop = mark_crop(image)
print(f"source {SOURCE} {image.size}, mark at {crop}")
outputs = {
"StoreLogo.png": tile(image, crop, 50),
"Square44x44Logo.png": tile(image, crop, 44),
"Square150x150Logo.png": tile(image, crop, 150),
"Square310x310Logo.png": tile(image, crop, 310),
"Wide310x150Logo.png": wide(image, crop, 310, 150),
}
for name, canvas in outputs.items():
path = os.path.join(OUT_DIR, name)
canvas.save(path, "PNG")
print(f"wrote {os.path.relpath(path, ROOT)} {canvas.size}")
if __name__ == "__main__":
main()
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Build the Microsoft Store MSIX package (TASK 48 item 2).
#
# scripts/package-msix.sh
#
# Publishes the app self-contained for win-x64 (so the package carries its own .NET
# runtime and the ONLY native folders are win-x64 — a RID-less publish copies every
# ONNX Runtime native folder), stages the manifest + logos from Packaging/, and packs
# with the Windows SDK's makeappx.
#
# The result is UNSIGNED (the Store signs the submitted package). To produce a package
# you can install locally for testing, point MSIX_PFX at a self-signed .pfx:
#
# MSIX_PFX=/path/to/dev.pfx MSIX_PFX_PASSWORD=... scripts/package-msix.sh
#
# ⚠️ Before a real submission: set Identity/@Publisher and Properties/P
# PublisherDisplayName in Packaging/Package.appxmanifest to the exact values Partner
# Center shows, and bump Identity/@Version. A mismatch is a certification rejection.
set -euo pipefail
REPO_WSL="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO_WIN="$(wslpath -w "$REPO_WSL")"
DOTNET="/mnt/c/Program Files/dotnet/dotnet.exe"
STAGE_WSL="$REPO_WSL/artifacts/msix-stage"
OUT_WSL="$REPO_WSL/artifacts/LlamaCasty.msix"
MAKEAPPX="$(ls -d "/mnt/c/Program Files (x86)/Windows Kits/10/bin/"*/x64/makeappx.exe 2>/dev/null \
| sort -V | tail -1 || true)"
SIGNTOOL="$(ls -d "/mnt/c/Program Files (x86)/Windows Kits/10/bin/"*/x64/signtool.exe 2>/dev/null \
| sort -V | tail -1 || true)"
[ -x "$MAKEAPPX" ] || { echo "makeappx.exe not found — install the Windows 10/11 SDK." >&2; exit 1; }
echo "== publish (Release, win-x64, self-contained) =="
rm -rf "$STAGE_WSL"
mkdir -p "$STAGE_WSL"
"$DOTNET" publish "$REPO_WIN\\ytLive.csproj" \
-c Release -r win-x64 --self-contained true \
--nologo -v minimal \
-o "$(wslpath -w "$STAGE_WSL")"
[ -f "$STAGE_WSL/ytLive.exe" ] || { echo "publish produced no ytLive.exe" >&2; exit 1; }
[ -d "$STAGE_WSL/ffmpeg" ] || {
echo "ffmpeg/ is missing from the package — it must ship bundled (Store policy 10.2.2)." >&2
exit 1
}
echo "== stage the manifest + logos =="
cp "$REPO_WSL/Packaging/Package.appxmanifest" "$STAGE_WSL/AppxManifest.xml"
cp -r "$REPO_WSL/Packaging/Assets" "$STAGE_WSL/Assets"
echo "== pack =="
"$MAKEAPPX" pack /d "$(wslpath -w "$STAGE_WSL")" /p "$(wslpath -w "$OUT_WSL")" /o
if [ -n "${MSIX_PFX:-}" ]; then
[ -x "$SIGNTOOL" ] || { echo "signtool.exe not found — install the Windows 10/11 SDK." >&2; exit 1; }
echo "== sign =="
if [ -n "${MSIX_PFX_PASSWORD:-}" ]; then
"$SIGNTOOL" sign /fd SHA256 /a /f "$(wslpath -w "$MSIX_PFX")" /p "$MSIX_PFX_PASSWORD" \
"$(wslpath -w "$OUT_WSL")"
else
"$SIGNTOOL" sign /fd SHA256 /a /f "$(wslpath -w "$MSIX_PFX")" "$(wslpath -w "$OUT_WSL")"
fi
else
echo "note: unsigned package (the Store signs submissions); set MSIX_PFX to sign locally."
fi
echo
echo "MSIX: $OUT_WSL ($(du -h "$OUT_WSL" | cut -f1))"
+74
View File
@@ -63,4 +63,78 @@ public class StorePackagingTests
.Where(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}"))
.Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}"));
}
private static readonly XNamespace Foundation =
"http://schemas.microsoft.com/appx/manifest/foundation/windows10";
private static readonly XNamespace Uap =
"http://schemas.microsoft.com/appx/manifest/uap/windows10";
private static readonly XNamespace Uap10 =
"http://schemas.microsoft.com/appx/manifest/uap/windows10/10";
private static readonly XNamespace Rescap =
"http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities";
/// <summary>TASK 48, slice 2 — the MSIX manifest. This is the file that decides whether
/// recording, capturing and the bundled ffmpeg still work once the app is packaged:
/// a full-trust packagedClassicApp at mediumIL can launch ffmpeg as a child process and
/// write to the user's profile; an AppContainer cannot, and recording would silently
/// land in a virtualised folder. So the guard pins every property that makes that true,
/// plus the x64/en-US decisions and the logo files the packer requires.</summary>
[Fact]
public void The_package_manifest_declares_full_trust_camera_and_microphone()
{
var manifestPath = SourceAssert.RepoPath(Path.Combine("Packaging", "Package.appxmanifest"));
Assert.True(File.Exists(manifestPath), "Packaging/Package.appxmanifest is missing.");
var manifest = XDocument.Load(manifestPath);
var package = manifest.Root!;
Assert.Equal(Foundation + "Package", package.Name);
// 1. Identity: x64 only (creator ruling 2026-10-09), and the title the Store expects.
var identity = package.Element(Foundation + "Identity")!;
Assert.Equal("LlamaCasty", (string?)identity.Attribute("Name"));
Assert.Equal("x64", (string?)identity.Attribute("ProcessorArchitecture"));
// 2. Full trust: packagedClassicApp + mediumIL + the restricted runFullTrust capability.
var application = package.Element(Foundation + "Applications")!
.Element(Foundation + "Application")!;
Assert.Equal("Windows.FullTrustApplication", (string?)application.Attribute("EntryPoint"));
Assert.Equal("ytLive.exe", (string?)application.Attribute("Executable"));
Assert.Equal("mediumIL", (string?)application.Attribute(Uap10 + "TrustLevel"));
Assert.Equal("packagedClassicApp", (string?)application.Attribute(Uap10 + "RuntimeBehavior"));
var capabilities = package.Element(Foundation + "Capabilities")!;
Assert.Contains(capabilities.Elements(Rescap + "Capability"),
element => (string?)element.Attribute("Name") == "runFullTrust");
var devices = capabilities.Elements(Foundation + "DeviceCapability")
.Select(element => (string?)element.Attribute("Name"))
.ToArray();
Assert.Contains("webcam", devices);
Assert.Contains("microphone", devices);
// 3. ⛔ Never appContainer — that is the change that breaks recording (item 6).
// Matched on ATTRIBUTE VALUES, not the file text: the manifest's own comment
// explains why it must not be appContainer, and prose is allowed to say so.
Assert.DoesNotContain(
manifest.Descendants()
.SelectMany(element => element.Attributes())
.Select(attribute => attribute.Value),
value => string.Equals(value, "appContainer", StringComparison.OrdinalIgnoreCase));
// 4. English only (policy 10.7): a second language forces the listing to be localised.
var languages = package.Element(Foundation + "Resources")!
.Elements(Foundation + "Resource")
.Select(element => (string?)element.Attribute("Language"))
.ToArray();
Assert.Equal(new[] { "en-US" }, languages);
// 5. Every image the manifest references is actually in the package source tree —
// makeappx refuses to pack otherwise.
var packaging = Path.GetDirectoryName(manifestPath)!;
foreach (var logo in manifest.Descendants()
.SelectMany(element => element.Attributes())
.Where(attribute => attribute.Name.LocalName.EndsWith("Logo", StringComparison.Ordinal))
.Select(attribute => attribute.Value.Replace('\\', Path.DirectorySeparatorChar)))
{
Assert.True(File.Exists(Path.Combine(packaging, logo)), $"manifest references a missing logo: {logo}");
}
}
}