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
+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}");
}
}
}