1+ using System . IO . Compression ;
12using System . Reflection ;
23using System . Security ;
34using System . Text ;
@@ -8,6 +9,75 @@ namespace Winsdk.Cli;
89
910internal 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 ( ) ;
0 commit comments