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:
@@ -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
@@ -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
@@ -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…`,
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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))"
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user