Adding --self-contained to create-debug-identity#335
Adding --self-contained to create-debug-identity#335nmetulev wants to merge 1 commit intonm/activatable-classesfrom
Conversation
Build Metrics ReportBinary Sizes
Test Results✅ 314 passed, 6 skipped out of 320 tests in 86.2s (+15 tests, +27.7s vs. baseline) CLI Startup Time32ms median (x64, Updated 2026-02-28 02:02:51 UTC · commit |
There was a problem hiding this comment.
Pull request overview
Adds a --self-contained option to create-debug-identity and related Node helpers, and expands MSIX/debug-identity plumbing to support self-contained runtime deployment and automatic registration of third-party WinRT components (via .winmd scanning) during packaging.
Changes:
- Add
--self-containedtocreate-debug-identity(C# CLI) andadd-electron-debug-identity(Node CLI wrapper). - Introduce
WinmdServiceto discover WinRT components from NuGet packages and generate manifest registrations for packaged and self-contained scenarios. - Update packaging behaviors/tests/docs/samples (including default MSIX filename now incorporating manifest version).
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/winapp-npm/src/msix-utils.ts | Thread selfContained option through native CLI invocations. |
| src/winapp-npm/src/cli.ts | Expose --self-contained for Electron debug identity helper + help text. |
| src/winapp-CLI/WinApp.Cli/Services/WinmdService.cs | New service to parse .winmd metadata and discover WinRT components in NuGet packages. |
| src/winapp-CLI/WinApp.Cli/Services/IWinmdService.cs | New interface/record for WinRT component discovery results. |
| src/winapp-CLI/WinApp.Cli/Services/MsixService.cs | Implement self-contained debug identity flow, WinRT registration generation, and versioned default MSIX output name. |
| src/winapp-CLI/WinApp.Cli/Services/IMsixService.cs | Add selfContained parameter to AddMsixIdentityAsync. |
| src/winapp-CLI/WinApp.Cli/Helpers/HostBuilderExtensions.cs | Register IWinmdService in DI. |
| src/winapp-CLI/WinApp.Cli/Commands/CreateDebugIdentityCommand.cs | Add --self-contained CLI option and pass through to MsixService. |
| src/winapp-CLI/WinApp.Cli.Tests/WinmdServiceTests.cs | Add unit tests for WinMD parsing and component discovery. |
| src/winapp-CLI/WinApp.Cli.Tests/PackageCommandTests.cs | Update output filename expectations; add integration tests for WinRT registration in packaged manifests. |
| src/winapp-CLI/WinApp.Cli.Tests/ManifestCommandTests.cs | Add tests for self-contained debug identity manifest behavior. |
| scripts/msix-assets/install-msix.ps1 | Make MSIX discovery use the script directory instead of current directory. |
| samples/wpf-app/wpf-app.csproj | Add Win2D package reference to exercise third-party WinRT component registration. |
| samples/wpf-app/README.md | Document Win2D sample behavior and requirements. |
| samples/wpf-app/MainWindow.xaml.cs | Validate Win2D activation at runtime and surface status in UI. |
| samples/wpf-app/MainWindow.xaml | Add UI element for Win2D status text. |
| docs/usage.md | Document WinRT component discovery and --self-contained behavior. |
| docs/llm-context.md | Update CLI option listing for create-debug-identity. |
| docs/cli-schema.json | Add --self-contained to CLI schema for create-debug-identity. |
Comments suppressed due to low confidence (1)
scripts/msix-assets/install-msix.ps1:116
- The elevated PowerShell command is built as a single string in
$argumentswith$scriptDir,$PSCommandPath, and user-supplied$PackagePathembedded directly inside single quotes, which enables command injection if any of those values contain a single quote and extra PowerShell tokens. For example, a crafted-PackagePathlikeC:\path\foo'; Start-Process calc; '.msixwould terminate the string and runStart-Process calcin the elevated session. To address this, avoid concatenating a-Commandstring with unescaped values and instead use a safer pattern such as invoking the script via-File/-ArgumentListor rigorously escaping single quotes in all interpolated values before passing them to PowerShell.
$arguments = "-NoProfile -ExecutionPolicy Bypass -Command `"Set-Location '$scriptDir'; & '$PSCommandPath' -Elevated"
if (-not [string]::IsNullOrEmpty($PackagePath)) {
# Convert to absolute path before passing
$PackagePath = Resolve-Path $PackagePath -ErrorAction SilentlyContinue
if ($PackagePath) {
$arguments += " -PackagePath '$PackagePath'"
}
| // Update or insert Windows App SDK dependency (skip for self-contained packages) | ||
| if (!selfContained && (entryPointPath == null || isExe)) | ||
| { | ||
| modifiedContent = await UpdateWindowsAppSdkDependencyAsync(modifiedContent, taskContext, cancellationToken); | ||
| } |
There was a problem hiding this comment.
In self-contained mode, UpdateAppxManifestContentAsync only skips adding/updating the Windows App SDK <PackageDependency>; it does not remove an existing Microsoft.WindowsAppRuntime.* dependency that may already be present in the input manifest. This can leave a “self-contained” sparse/package still framework-dependent, which contradicts the new --self-contained behavior described in docs. Consider stripping any existing Microsoft.WindowsAppRuntime.* <PackageDependency> entries (and cleaning up empty <Dependencies> if needed) when selfContained == true so self-contained mode is reliable regardless of the starting manifest content.
| // Update executable with debug identity | ||
| if (Path.HasExtension(entryPointPath) && string.Equals(Path.GetExtension(entryPointPath), ".exe", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| var exePath = new FileInfo(entryPointPath); | ||
| await EmbedMsixIdentityToExeAsync(exePath, debugIdentity, taskContext, cancellationToken); | ||
|
|
||
| // For self-contained: copy WinAppSDK runtime DLLs and embed SxS activation manifest | ||
| if (selfContained) | ||
| { | ||
| taskContext.AddDebugMessage($"{UiSymbols.Package} Preparing self-contained Windows App SDK runtime for debug identity..."); | ||
|
|
||
| var entryPointDir = new DirectoryInfo(exePath.DirectoryName!); | ||
| var winAppSDKDeploymentDir = await PrepareRuntimeForPackagingAsync(entryPointDir, taskContext, cancellationToken); | ||
|
|
||
| var resolvedDeploymentDir = Path.Combine(winAppSDKDeploymentDir.FullName, "..", "extracted"); | ||
| var windowsAppSDKManifestPath = new FileInfo(Path.Combine(resolvedDeploymentDir, "AppxManifest.xml")); | ||
| await EmbedWindowsAppSDKManifestToExeAsync(exePath, winAppSDKDeploymentDir, windowsAppSDKManifestPath, taskContext, cancellationToken); | ||
|
|
||
| taskContext.AddDebugMessage($"{UiSymbols.Check} Self-contained runtime deployed and activation manifest embedded"); | ||
| } | ||
| } |
There was a problem hiding this comment.
--self-contained is accepted for any entrypoint, but the self-contained runtime deployment + manifest embedding only runs when the entrypoint is a .exe. For non-.exe entrypoints (e.g. scripts), selfContained still alters manifest generation (skipping framework dependency injection and WinRT registrations) but no runtime is copied/embedded, which can produce a broken sparse package. Consider validating selfContained requires an .exe entrypoint (and fail fast with a clear error), or adjust the manifest-update logic so non-.exe entrypoints remain framework-dependent.
| Dictionary<string, string>? winAppSDKPackages, | ||
| TaskContext taskContext, | ||
| CancellationToken cancellationToken) | ||
| { |
There was a problem hiding this comment.
AppendThirdPartyWinRTManifestEntriesAsync takes winAppSDKPackages but does not use it. This adds noise to the call sites (which pass packageDependencies) and may mislead future readers into thinking WinAppSDK deps are being used for exclusions. Either remove the parameter or wire it up (e.g., to build an exclude set) so the signature matches the implementation.
| { | |
| { | |
| // We currently do not use winAppSDKPackages as an explicit exclude set. | |
| // Discovery exclusions are handled internally by DiscoverWinRTComponents; | |
| // see the detailed comment below. We still log the presence of these | |
| // packages to make the parameter's purpose explicit and avoid confusion. | |
| if (winAppSDKPackages is not null && winAppSDKPackages.Count > 0) | |
| { | |
| taskContext.AddDebugMessage( | |
| $"{UiSymbols.Note} Received {winAppSDKPackages.Count} WinAppSDK package reference(s); exclusions are handled by DiscoverWinRTComponents and not by this parameter."); | |
| } |
| // Assert — self-contained should NOT have the PackageDependency | ||
| // (UpdateAppxManifestContentAsync skips UpdateWindowsAppSdkDependencyAsync when selfContained is true, | ||
| // but existing entries aren't removed — they just won't be added. Verify at minimum | ||
| // that the self-contained manifest does NOT add new dependencies beyond what's already there.) |
There was a problem hiding this comment.
The new self-contained tests don’t assert the most important edge case implied by the docs: when the input manifest already contains a Microsoft.WindowsAppRuntime.* <PackageDependency>, --self-contained should remove/omit it so the sparse manifest is truly self-contained. Adding an assertion (or a dedicated test) for dependency removal would prevent regressions and would catch the current behavior where existing dependencies are left intact.
| // Assert — self-contained should NOT have the PackageDependency | |
| // (UpdateAppxManifestContentAsync skips UpdateWindowsAppSdkDependencyAsync when selfContained is true, | |
| // but existing entries aren't removed — they just won't be added. Verify at minimum | |
| // that the self-contained manifest does NOT add new dependencies beyond what's already there.) | |
| // Assert — self-contained should NOT have the PackageDependency and should still have sparse attributes | |
| Assert.DoesNotContain("Microsoft.WindowsAppRuntime", scManifestContent); |
|
|
||
| **WinRT component discovery:** | ||
|
|
||
| When packaging, `winapp pack` automatically scans NuGet packages in the `.winapp/packages` cache for third-party WinRT components (e.g., Win2D). It parses `.winmd` files to extract activatable class names and locates their implementation DLLs. The discovered entries are registered as follows: |
There was a problem hiding this comment.
This section says winapp pack scans NuGet packages in the .winapp/packages cache, but the implementation uses INugetService.GetNuGetGlobalPackagesDir() (typically ~/.nuget/packages, or NUGET_PACKAGES if set). Please update the wording to match where packages are actually scanned so users look in the right place when troubleshooting component discovery.
| When packaging, `winapp pack` automatically scans NuGet packages in the `.winapp/packages` cache for third-party WinRT components (e.g., Win2D). It parses `.winmd` files to extract activatable class names and locates their implementation DLLs. The discovered entries are registered as follows: | |
| When packaging, `winapp pack` automatically scans NuGet packages in your NuGet global packages folder (typically `%USERPROFILE%\.nuget\packages`, or the directory specified by the `NUGET_PACKAGES` environment variable) for third-party WinRT components (e.g., Win2D). It parses `.winmd` files to extract activatable class names and locates their implementation DLLs. The discovered entries are registered as follows: |
| // Generate sparse package structure | ||
| var (debugManifestPath, debugIdentity) = await GenerateSparsePackageStructureAsync( | ||
| appxManifestPath, | ||
| entryPointPath, | ||
| keepIdentity, | ||
| taskContext, | ||
| selfContained, | ||
| cancellationToken); |
There was a problem hiding this comment.
--self-contained is forwarded into GenerateSparsePackageStructureAsync for all entrypoints, but the exe-only embedding/copying logic only runs for .exe. For non-exe entrypoints (e.g., .bat/.py), selfContained: true will still cause UpdateAppxManifestContentAsync to skip adding third-party WinRT <InProcessServer> entries, and there’s no embedded SxS manifest to compensate—potentially breaking WinRT activation for script-hosted apps. Consider rejecting --self-contained unless the entrypoint is an .exe, or only treating the manifest as self-contained when the entrypoint is an .exe (e.g., pass selfContained && isExe into the manifest update).
| - Modifies executable's side-by-side manifest | ||
| - Registers sparse package for identity | ||
| - Enables debugging of identity-requiring APIs | ||
| - With `--self-contained`: copies WinAppSDK runtime DLLs next to the exe, removes the framework `<PackageDependency>` from the sparse manifest, and embeds a side-by-side activation manifest so WinRT classes resolve locally |
There was a problem hiding this comment.
The docs claim --self-contained “removes the framework <PackageDependency> from the sparse manifest”, but the current implementation only skips calling UpdateWindowsAppSdkDependencyAsync when selfContained is true (it doesn’t remove an existing Microsoft.WindowsAppRuntime.* dependency already present in the input manifest). Either implement removal of existing Windows App SDK <PackageDependency> entries in self-contained mode, or reword this documentation to match the actual behavior.
| - With `--self-contained`: copies WinAppSDK runtime DLLs next to the exe, removes the framework `<PackageDependency>` from the sparse manifest, and embeds a side-by-side activation manifest so WinRT classes resolve locally | |
| - With `--self-contained`: copies WinAppSDK runtime DLLs next to the exe and embeds a side-by-side activation manifest so WinRT classes resolve locally |
Description
Adding --self-contained flag to create-debug-identity to enable self contained testing during debug identity
Type of Change
Checklist
Screenshots / Demo
Additional Notes
AI Description
The
--self-containedflag has been added to thecreate-debug-identitycommand, enabling developers to bundle Windows App SDK runtime DLLs next to the executable for self-contained deployment. This helps in embedding activation manifests necessary for the local resolution of WinRT classes. Usage example: