Skip to content

Adding --self-contained to create-debug-identity#335

Draft
nmetulev wants to merge 1 commit intonm/activatable-classesfrom
nm/debug-identity-self-contained
Draft

Adding --self-contained to create-debug-identity#335
nmetulev wants to merge 1 commit intonm/activatable-classesfrom
nm/debug-identity-self-contained

Conversation

@nmetulev
Copy link
Member

@nmetulev nmetulev commented Feb 28, 2026

Description

Adding --self-contained flag to create-debug-identity to enable self contained testing during debug identity

Type of Change

  • ✨ New feature

Checklist

Screenshots / Demo

Additional Notes

AI Description

The --self-contained flag has been added to the create-debug-identity command, 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:

winapp create-debug-identity ./bin/MyApp.exe --self-contained

@github-actions github-actions bot added the enhancement New feature or request label Feb 28, 2026
@nmetulev nmetulev requested a review from Copilot February 28, 2026 01:57
@github-actions
Copy link

Build Metrics Report

Binary Sizes

Artifact Baseline Current Delta
CLI (ARM64) 13.70 MB 13.83 MB 📈 +130.0 KB (+0.93%)
CLI (x64) 13.01 MB 13.13 MB 📈 +124.0 KB (+0.93%)
MSIX (ARM64) 5.96 MB 6.02 MB 📈 +53.9 KB (+0.88%)
MSIX (x64) 6.20 MB 6.25 MB 📈 +50.6 KB (+0.80%)
NPM Package 12.14 MB 12.25 MB 📈 +116.2 KB (+0.93%)

Test Results

314 passed, 6 skipped out of 320 tests in 86.2s (+15 tests, +27.7s vs. baseline)

CLI Startup Time

32ms median (x64, winapp --version) · ✅ no change vs. baseline


Updated 2026-02-28 02:02:51 UTC · commit 04563fc · workflow run

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-contained to create-debug-identity (C# CLI) and add-electron-debug-identity (Node CLI wrapper).
  • Introduce WinmdService to 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 $arguments with $scriptDir, $PSCommandPath, and user-supplied $PackagePath embedded 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 -PackagePath like C:\path\foo'; Start-Process calc; '.msix would terminate the string and run Start-Process calc in the elevated session. To address this, avoid concatenating a -Command string with unescaped values and instead use a safer pattern such as invoking the script via -File/-ArgumentList or 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'"
            }

Comment on lines 1926 to 1930
// Update or insert Windows App SDK dependency (skip for self-contained packages)
if (!selfContained && (entryPointPath == null || isExe))
{
modifiedContent = await UpdateWindowsAppSdkDependencyAsync(modifiedContent, taskContext, cancellationToken);
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 332 to 352
// 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");
}
}
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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.

Copilot uses AI. Check for mistakes.
Dictionary<string, string>? winAppSDKPackages,
TaskContext taskContext,
CancellationToken cancellationToken)
{
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
{
{
// 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.");
}

Copilot uses AI. Check for mistakes.
Comment on lines +546 to +549
// 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.)
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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);

Copilot uses AI. Check for mistakes.

**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:
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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:

Copilot uses AI. Check for mistakes.
@nmetulev nmetulev changed the base branch from main to nm/activatable-classes March 2, 2026 23:13
@nmetulev nmetulev requested a review from Copilot March 2, 2026 23:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment on lines 323 to 330
// Generate sparse package structure
var (debugManifestPath, debugIdentity) = await GenerateSparsePackageStructureAsync(
appxManifestPath,
entryPointPath,
keepIdentity,
taskContext,
selfContained,
cancellationToken);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--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).

Copilot uses AI. Check for mistakes.
- 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
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- 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

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants