Skip to content

Commit 2eecb25

Browse files
committed
[WIP] selfcontained.
1 parent 6f8193d commit 2eecb25

8 files changed

Lines changed: 515 additions & 6 deletions

File tree

src/winsdk-CLI/Winsdk.Cli/Commands/MsixPackageCommand.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ public MsixPackageCommand()
5252
{
5353
Description = "Publisher name for certificate generation"
5454
};
55+
var selfContainedOption = new Option<bool>("--self-contained")
56+
{
57+
Description = "Bundle Windows App SDK runtime for self-contained deployment"
58+
};
5559

5660
Options.Add(nameOption);
5761
Options.Add(skipPriOption);
@@ -60,6 +64,7 @@ public MsixPackageCommand()
6064
Options.Add(generateCertOption);
6165
Options.Add(installCertOption);
6266
Options.Add(publisherOption);
67+
Options.Add(selfContainedOption);
6368
Options.Add(Program.VerboseOption);
6469

6570
SetAction(async (parseResult, ct) =>
@@ -73,6 +78,7 @@ public MsixPackageCommand()
7378
var generateCert = parseResult.GetValue(generateCertOption);
7479
var installCert = parseResult.GetValue(installCertOption);
7580
var publisher = parseResult.GetValue(publisherOption);
81+
var selfContained = parseResult.GetValue(selfContainedOption);
7682
var verbose = parseResult.GetValue(Program.VerboseOption);
7783

7884
try
@@ -82,7 +88,7 @@ public MsixPackageCommand()
8288

8389
var msix = new MsixService();
8490

85-
var result = await msix.CreateMsixPackageAsync(inputFolder, outputFolder, name, skipPri, autoSign, certPath, certPassword, generateCert, installCert, publisher, verbose, ct);
91+
var result = await msix.CreateMsixPackageAsync(inputFolder, outputFolder, name, skipPri, autoSign, certPath, certPassword, generateCert, installCert, publisher, selfContained, verbose, ct);
8692

8793
Console.WriteLine("✅ MSIX package created successfully!");
8894

src/winsdk-CLI/Winsdk.Cli/Services/MsixService.cs

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.IO.Compression;
12
using System.Reflection;
23
using System.Security;
34
using System.Text;
@@ -8,6 +9,75 @@ namespace Winsdk.Cli;
89

910
internal sealed class MsixService
1011
{
12+
/// <summary>
13+
/// Sets up Windows App SDK for self-contained deployment by extracting MSIX content
14+
/// and preparing the necessary files for embedding in applications.
15+
/// </summary>
16+
public async Task SetupSelfContainedAsync(string winsdkDir, string packagesDir, string wasdkVersion, string architecture, bool verbose, CancellationToken cancellationToken = default)
17+
{
18+
// Look for the Runtime package which contains the MSIX files
19+
var runtimePackageDir = Path.Combine(packagesDir, $"Microsoft.WindowsAppSDK.Runtime.{wasdkVersion}");
20+
if (!Directory.Exists(runtimePackageDir))
21+
{
22+
throw new DirectoryNotFoundException($"Windows App SDK Runtime package directory not found: {runtimePackageDir}");
23+
}
24+
25+
var selfContainedDir = Path.Combine(winsdkDir, "self-contained");
26+
Directory.CreateDirectory(selfContainedDir);
27+
28+
var archSelfContainedDir = Path.Combine(selfContainedDir, architecture);
29+
Directory.CreateDirectory(archSelfContainedDir);
30+
31+
// Look for the MSIX file in the tools/MSIX folder
32+
var msixToolsDir = Path.Combine(runtimePackageDir, "tools", "MSIX", $"win10-{architecture}");
33+
if (!Directory.Exists(msixToolsDir))
34+
{
35+
throw new DirectoryNotFoundException($"MSIX tools directory not found: {msixToolsDir}");
36+
}
37+
38+
var msixFiles = Directory.GetFiles(msixToolsDir, "Microsoft.WindowsAppRuntime.*.msix");
39+
if (msixFiles.Length == 0)
40+
{
41+
throw new FileNotFoundException($"No MSIX files found in {msixToolsDir}");
42+
}
43+
44+
// Use the main MSIX file (not DDLM or Singleton)
45+
var msixPath = msixFiles.FirstOrDefault(f => Path.GetFileName(f).Contains("Main")) ?? msixFiles[0];
46+
47+
if (verbose)
48+
{
49+
Console.WriteLine($" {UiSymbols.Package} Extracting MSIX: {Path.GetFileName(msixPath)}");
50+
}
51+
52+
// Extract MSIX content
53+
var extractedDir = Path.Combine(archSelfContainedDir, "extracted");
54+
if (Directory.Exists(extractedDir))
55+
{
56+
Directory.Delete(extractedDir, true);
57+
}
58+
Directory.CreateDirectory(extractedDir);
59+
60+
using (var archive = ZipFile.OpenRead(msixPath))
61+
{
62+
archive.ExtractToDirectory(extractedDir);
63+
}
64+
65+
// Copy relevant files to deployment directory
66+
var deploymentDir = Path.Combine(archSelfContainedDir, "deployment");
67+
Directory.CreateDirectory(deploymentDir);
68+
69+
// Copy DLLs, WinMD files, and other runtime assets
70+
await CopyRuntimeFilesAsync(extractedDir, deploymentDir, verbose);
71+
72+
// Generate self-contained manifest template using embedded template
73+
await GenerateSelfContainedManifestTemplateAsync(archSelfContainedDir, verbose, cancellationToken);
74+
75+
if (verbose)
76+
{
77+
Console.WriteLine($" {UiSymbols.Check} Self-contained files prepared in: {archSelfContainedDir}");
78+
}
79+
}
80+
1181
public async Task GenerateMsixAssetsAsync(bool isSparse, string outputDir, string? packageName, string? publisherName, string description, string version, string? executable, CancellationToken cancellationToken = default)
1282
{
1383
var defaults = new SystemDefaultsService();
@@ -349,6 +419,7 @@ public async Task<CreateMsixPackageResult> CreateMsixPackageAsync(
349419
bool generateDevCert = false,
350420
bool installDevCert = false,
351421
string? publisher = null,
422+
bool selfContained = false,
352423
bool verbose = true,
353424
CancellationToken cancellationToken = default)
354425
{
@@ -419,6 +490,17 @@ public async Task<CreateMsixPackageResult> CreateMsixPackageAsync(
419490
await GeneratePriFileAsync(inputFolder, verbose: verbose, cancellationToken: cancellationToken);
420491
}
421492

493+
// Handle self-contained deployment if requested
494+
if (selfContained)
495+
{
496+
if (verbose)
497+
{
498+
Console.WriteLine($"{UiSymbols.Package} Preparing self-contained Windows App SDK runtime...");
499+
}
500+
501+
await PrepareRuntimeForPackagingAsync(inputFolder, verbose, cancellationToken);
502+
}
503+
422504
// Create MSIX package
423505
var makeappxArguments = $@"pack /o /d ""{inputFolder}"" /nv /p ""{outputMsixPath}""";
424506

@@ -541,6 +623,201 @@ private static string ToCamelCase(string name)
541623
return sb.ToString();
542624
}
543625

626+
private Task CopyRuntimeFilesAsync(string extractedDir, string deploymentDir, bool verbose)
627+
{
628+
var patterns = new[] { "*.dll", "*.winmd", "*.mui", "*.pri" };
629+
630+
foreach (var pattern in patterns)
631+
{
632+
var files = Directory.GetFiles(extractedDir, pattern, SearchOption.AllDirectories);
633+
foreach (var file in files)
634+
{
635+
var relativePath = Path.GetRelativePath(extractedDir, file);
636+
var destPath = Path.Combine(deploymentDir, relativePath);
637+
638+
// Create destination directory if needed
639+
var destDir = Path.GetDirectoryName(destPath);
640+
if (!string.IsNullOrEmpty(destDir))
641+
{
642+
Directory.CreateDirectory(destDir);
643+
}
644+
645+
File.Copy(file, destPath, overwrite: true);
646+
647+
if (verbose)
648+
{
649+
Console.WriteLine($" {UiSymbols.Files} {relativePath}");
650+
}
651+
}
652+
}
653+
return Task.CompletedTask;
654+
}
655+
656+
private async Task GenerateSelfContainedManifestTemplateAsync(string outputDir, bool verbose, CancellationToken cancellationToken)
657+
{
658+
var templateResName = FindResourceEnding(".Templates.app.manifest.selfcontained.xml")
659+
?? throw new FileNotFoundException("Self-contained manifest template not found in embedded resources");
660+
661+
// Use embedded template
662+
var asm = Assembly.GetExecutingAssembly();
663+
string template;
664+
await using (var s = asm.GetManifestResourceStream(templateResName) ?? throw new FileNotFoundException(templateResName))
665+
using (var sr = new StreamReader(s, Encoding.UTF8))
666+
{
667+
template = await sr.ReadToEndAsync(cancellationToken);
668+
}
669+
670+
var manifestTemplatePath = Path.Combine(outputDir, "app.manifest.template");
671+
await File.WriteAllTextAsync(manifestTemplatePath, template, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), cancellationToken);
672+
673+
if (verbose)
674+
{
675+
Console.WriteLine($" {UiSymbols.Files} Generated manifest template: {Path.GetFileName(manifestTemplatePath)}");
676+
}
677+
}
678+
679+
/// <summary>
680+
/// Prepares Windows App SDK runtime files for packaging into an MSIX by extracting them to the input folder
681+
/// </summary>
682+
private async Task PrepareRuntimeForPackagingAsync(string inputFolder, bool verbose, CancellationToken cancellationToken)
683+
{
684+
// Find .winsdk directory to determine where packages are installed
685+
var currentDir = Directory.GetCurrentDirectory();
686+
var winsdkDir = BuildToolsService.FindWinsdkDirectory(currentDir);
687+
var pkgsDir = Path.Combine(winsdkDir, "packages");
688+
689+
if (!Directory.Exists(pkgsDir))
690+
{
691+
throw new DirectoryNotFoundException($"Packages directory not found: {pkgsDir}. Please run 'winsdk setup' first.");
692+
}
693+
694+
// Find Windows App SDK package
695+
var wasdkPattern = "Microsoft.WindowsAppSDK.*";
696+
var wasdkDirs = Directory.GetDirectories(pkgsDir, wasdkPattern);
697+
698+
if (wasdkDirs.Length == 0)
699+
{
700+
throw new DirectoryNotFoundException($"Windows App SDK package not found in {pkgsDir}. Please run 'winsdk setup' first.");
701+
}
702+
703+
var wasdkDir = wasdkDirs.OrderByDescending(d => d).First(); // Get the latest version
704+
var wasdkVersion = Path.GetFileName(wasdkDir).Replace("Microsoft.WindowsAppSDK.", "");
705+
706+
// Check if there's a specific Runtime package - use that version instead
707+
var runtimePattern = "Microsoft.WindowsAppSDK.Runtime.*";
708+
var runtimeDirs = Directory.GetDirectories(pkgsDir, runtimePattern);
709+
if (runtimeDirs.Length > 0)
710+
{
711+
var runtimeDir = runtimeDirs.OrderByDescending(d => d).First();
712+
wasdkVersion = Path.GetFileName(runtimeDir).Replace("Microsoft.WindowsAppSDK.Runtime.", "");
713+
}
714+
715+
if (verbose)
716+
{
717+
Console.WriteLine($"{UiSymbols.Package} Found Windows App SDK {wasdkVersion}");
718+
}
719+
720+
// Determine target architecture - for now use x64, but this could be made configurable
721+
var arch = "x64";
722+
723+
// Create temporary directory for runtime files
724+
var tempRuntimeDir = Path.Combine(Path.GetTempPath(), "winsdk-runtime-temp", Guid.NewGuid().ToString());
725+
Directory.CreateDirectory(tempRuntimeDir);
726+
727+
try
728+
{
729+
// Extract runtime files using the existing method
730+
await SetupSelfContainedAsync(winsdkDir, pkgsDir, wasdkVersion, arch, verbose, cancellationToken);
731+
732+
// Copy runtime files from .winsdk/self-contained to input folder
733+
var runtimeSourceDir = Path.Combine(winsdkDir, "self-contained", arch, "deployment");
734+
var runtimeDestDir = Path.Combine(inputFolder, "WinAppSDK");
735+
736+
if (Directory.Exists(runtimeSourceDir))
737+
{
738+
Directory.CreateDirectory(runtimeDestDir);
739+
740+
foreach (var file in Directory.GetFiles(runtimeSourceDir))
741+
{
742+
var destFile = Path.Combine(runtimeDestDir, Path.GetFileName(file));
743+
File.Copy(file, destFile, overwrite: true);
744+
745+
if (verbose)
746+
{
747+
Console.WriteLine($"{UiSymbols.Folder} Bundled runtime: {Path.GetFileName(file)}");
748+
}
749+
}
750+
751+
if (verbose)
752+
{
753+
Console.WriteLine($"{UiSymbols.Check} Windows App SDK runtime bundled into package");
754+
}
755+
}
756+
else
757+
{
758+
throw new DirectoryNotFoundException($"Runtime files not found at {runtimeSourceDir}");
759+
}
760+
}
761+
finally
762+
{
763+
// Clean up temp directory
764+
if (Directory.Exists(tempRuntimeDir))
765+
{
766+
Directory.Delete(tempRuntimeDir, recursive: true);
767+
}
768+
}
769+
}
770+
771+
/// <summary>
772+
/// Cleans and sanitizes a package name to meet MSIX AppxManifest schema requirements.
773+
/// Based on ST_PackageName type which restricts ST_AsciiIdentifier.
774+
/// </summary>
775+
/// <param name="packageName">The package name to clean</param>
776+
/// <returns>A cleaned package name that meets MSIX schema requirements</returns>
777+
private static string CleanPackageName(string packageName)
778+
{
779+
if (string.IsNullOrWhiteSpace(packageName))
780+
{
781+
return "DefaultPackage";
782+
}
783+
784+
// Trim whitespace
785+
var cleaned = packageName.Trim();
786+
787+
// Remove invalid characters (keep only letters, numbers, hyphens, underscores, periods, and spaces)
788+
// ST_AllowedAsciiCharSet pattern="[-_. A-Za-z0-9]+"
789+
cleaned = Regex.Replace(cleaned, @"[^A-Za-z0-9\-_. ]", "");
790+
791+
// Remove leading underscores (ST_AsciiIdentifier restriction)
792+
cleaned = cleaned.TrimStart('_');
793+
794+
// If still empty or whitespace after cleaning, use default
795+
if (string.IsNullOrWhiteSpace(cleaned))
796+
{
797+
cleaned = "DefaultPackage";
798+
}
799+
800+
// Ensure minimum length of 3 characters
801+
if (cleaned.Length < 3)
802+
{
803+
cleaned = cleaned.PadRight(3, '1'); // Pad with '1' to reach minimum length
804+
}
805+
806+
// Truncate to maximum length of 50 characters
807+
if (cleaned.Length > 50)
808+
{
809+
cleaned = cleaned.Substring(0, 50).TrimEnd(); // Trim end in case we cut off mid-word
810+
}
811+
812+
// Final check: ensure it doesn't start with underscore after all transformations
813+
if (cleaned.StartsWith('_'))
814+
{
815+
cleaned = string.Concat("App", cleaned.AsSpan(1));
816+
}
817+
818+
return cleaned;
819+
}
820+
544821
private static string? FindResourceEnding(string endsWith)
545822
{
546823
var asm = Assembly.GetExecutingAssembly();
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version='1.0' encoding='utf-8' standalone='yes'?>
2+
<assembly manifestVersion='1.0'
3+
xmlns:asmv3='urn:schemas-microsoft-com:asm.v3'
4+
xmlns:winrtv1='urn:schemas-microsoft-com:winrt.v1'
5+
xmlns='urn:schemas-microsoft-com:asm.v1'>
6+
7+
<!--
8+
This is a template for Windows App SDK self-contained deployment.
9+
10+
To use this template:
11+
1. Copy this file to your project as app.manifest
12+
2. Update the assembly identity information as needed
13+
3. Add references to your WinRT components if any
14+
4. Include the deployment files from the deployment/ folder in your application
15+
16+
For complete setup, see the Microsoft.WindowsAppSDK.SelfContained.targets file.
17+
-->
18+
19+
<assemblyIdentity version="1.0.0.0" name="YourApp.exe" type="win32"/>
20+
21+
<!-- Windows App SDK Runtime Files -->
22+
<!-- Add asmv3:file entries for each DLL in the deployment folder -->
23+
<!-- Example:
24+
<asmv3:file name='Microsoft.WindowsAppRuntime.dll'/>
25+
<asmv3:file name='Microsoft.WindowsAppRuntime.Bootstrap.dll'/>
26+
-->
27+
28+
</assembly>

0 commit comments

Comments
 (0)