From 7a173ff33f3256a10986d1c6b077cc539ab892e6 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <michael.suchacz@gmail.com>
Date: Tue, 1 Apr 2025 10:13:32 +0000
Subject: [PATCH 1/3] added windows app sdk fetching and bundling into the
 installer

---
 Installer/Program.cs          | 42 ++++++++++++++++++++++++++++-------
 scripts/Get-WindowsAppSdk.ps1 | 34 ++++++++++++++++++++++++++++
 scripts/Publish.ps1           |  5 +++++
 scripts/files/.gitignore      |  1 +
 4 files changed, 74 insertions(+), 8 deletions(-)
 create mode 100644 scripts/Get-WindowsAppSdk.ps1

diff --git a/Installer/Program.cs b/Installer/Program.cs
index 7945f5b..eeb65d5 100644
--- a/Installer/Program.cs
+++ b/Installer/Program.cs
@@ -116,6 +116,9 @@ public class BootstrapperOptions : SharedOptions
     [Option('m', "msi-path", Required = true, HelpText = "Path to the MSI package to embed")]
     public string MsiPath { get; set; }
 
+    [Option('w', "windows-app-sdk-path", Required = true, HelpText = "Path to the Windows App Sdk package to embed")]
+    public string WindowsAppSdkPath { get; set; }
+
     public new void Validate()
     {
         base.Validate();
@@ -124,6 +127,8 @@ public class BootstrapperOptions : SharedOptions
             throw new ArgumentException($"Logo PNG file not found at '{LogoPng}'", nameof(LogoPng));
         if (!SystemFile.Exists(MsiPath))
             throw new ArgumentException($"MSI package not found at '{MsiPath}'", nameof(MsiPath));
+        if (!SystemFile.Exists(WindowsAppSdkPath))
+            throw new ArgumentException($"Windows App Sdk package not found at '{WindowsAppSdkPath}'", nameof(WindowsAppSdkPath));
     }
 }
 
@@ -337,16 +342,16 @@ private static int BuildBundle(BootstrapperOptions opts)
     {
         opts.Validate();
 
-        if (!DotNetRuntimePackagePayloads.TryGetValue(opts.Platform, out var payload))
+        if (!DotNetRuntimePackagePayloads.TryGetValue(opts.Platform, out var dotNetRuntimePayload))
             throw new ArgumentException($"Invalid architecture '{opts.Platform}' specified", nameof(opts.Platform));
 
-        // TODO: it would be nice to include the WindowsAppRuntime but
-        //       Microsoft makes it difficult to check from a regular
-        //       installer:
-        //       https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/check-windows-app-sdk-versions
-        //       https://github.com/microsoft/WindowsAppSDK/discussions/2437
+        var windowsAppSdkPaylod = new ExePackagePayload
+        {
+            SourceFile = opts.WindowsAppSdkPath
+        };
+
         var bundle = new Bundle(ProductName,
-            new ExePackage
+            new ExePackage // .NET Runtime
             {
                 PerMachine = true,
                 // Don't uninstall the runtime when the bundle is uninstalled.
@@ -362,7 +367,28 @@ private static int BuildBundle(BootstrapperOptions opts)
                 // anyway. The MSI will fatally exit if the runtime really isn't
                 // available, and the user can install it themselves.
                 Vital = false,
-                Payloads = [payload],
+                Payloads = [dotNetRuntimePayload],
+            },
+            // TODO: right now we are including the Windows App Sdk in the bundle
+            //       and always install it
+            //       Microsoft makes it difficult to check if it exists from a regular installer:
+            //       https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/check-windows-app-sdk-versions
+            //       https://github.com/microsoft/WindowsAppSDK/discussions/2437
+            new ExePackage // Windows App Sdk
+            {
+                PerMachine = true,
+                Permanent = true,
+                Cache = PackageCacheAction.remove,
+                // There is no license agreement for this SDK.
+                InstallArguments = "--quiet",
+                Vital = false,
+                Payloads = 
+                [
+                    new ExePackagePayload 
+                    {
+                        SourceFile = opts.WindowsAppSdkPath
+                    }
+                ],
             },
             new MsiPackage(opts.MsiPath)
             {
diff --git a/scripts/Get-WindowsAppSdk.ps1 b/scripts/Get-WindowsAppSdk.ps1
new file mode 100644
index 0000000..a9ca02a
--- /dev/null
+++ b/scripts/Get-WindowsAppSdk.ps1
@@ -0,0 +1,34 @@
+# Usage: Get-WindowsAppSdk.ps1 -arch <x64|arm64>
+param (
+    [ValidateSet("x64", "arm64")]
+    [Parameter(Mandatory = $true)]
+    [string] $arch
+)
+
+function Download-File([string] $url, [string] $outputPath, [string] $etagFile) {
+    Write-Host "Downloading '$url' to '$outputPath'"
+    # We use `curl.exe` here because `Invoke-WebRequest` is notoriously slow.
+    & curl.exe `
+        --progress-bar `
+        -v `
+        --show-error `
+        --fail `
+        --location `
+        --etag-compare $etagFile `
+        --etag-save $etagFile `
+        --output $outputPath `
+        $url
+    if ($LASTEXITCODE -ne 0) { throw "Failed to download $url" }
+    if (!(Test-Path $outputPath) -or (Get-Item $outputPath).Length -eq 0) {
+        throw "Failed to download '$url', output file '$outputPath' is missing or empty"
+    }
+}
+
+# Download the Windows App Sdk binary from Microsoft for this platform if we don't have
+# it yet (or it's different).
+$windowsAppSdkMajorVersion = "1.6"
+$windowsAppSdkFullVersion = "1.6.250228001"
+$windowsAppSdkPath = Join-Path $PSScriptRoot "files\windows-app-sdk-$($arch).exe"
+$windowsAppSdkUri = "https://aka.ms/windowsappsdk/$($windowsAppSdkMajorVersion)/$($windowsAppSdkFullVersion)/windowsappruntimeinstall-$($arch).exe"
+$windowsAppSdkEtagFile = $windowsAppSdkPath + ".etag"
+Download-File $windowsAppSdkUri $windowsAppSdkPath $windowsAppSdkEtagFile
\ No newline at end of file
diff --git a/scripts/Publish.ps1 b/scripts/Publish.ps1
index dccb39f..4390dfa 100644
--- a/scripts/Publish.ps1
+++ b/scripts/Publish.ps1
@@ -175,6 +175,10 @@ Copy-Item $mutagenAgentsSrcPath $mutagenAgentsDestPath
 if ($LASTEXITCODE -ne 0) { throw "Failed to build MSI" }
 Add-CoderSignature $msiOutputPath
 
+$getWindowsAppSdk = Join-Path $scriptRoot "Get-WindowsAppSdk.ps1"
+& $getWindowsAppSdk -arch $arch
+$windowsAppSdkPath = Join-Path $scriptRoot "files\windows-app-sdk-$($arch).exe"
+
 # Build the bootstrapper
 & dotnet.exe run --project .\Installer\Installer.csproj -c Release -- `
     build-bootstrapper `
@@ -184,6 +188,7 @@ Add-CoderSignature $msiOutputPath
     --output-path $outputPath `
     --icon-file "App\coder.ico" `
     --msi-path $msiOutputPath `
+    --windows-app-sdk-path $windowsAppSdkPath `
     --logo-png "scripts\files\logo.png"
 if ($LASTEXITCODE -ne 0) { throw "Failed to build bootstrapper" }
 
diff --git a/scripts/files/.gitignore b/scripts/files/.gitignore
index 859c764..9080d92 100644
--- a/scripts/files/.gitignore
+++ b/scripts/files/.gitignore
@@ -1,3 +1,4 @@
 mutagen-*.tar.gz
 mutagen-*.exe
 *.etag
+windows-app-sdk-*.exe
\ No newline at end of file

From 021e29c1107ed73ec5c374df32edfba589e86250 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <michael.suchacz@gmail.com>
Date: Tue, 1 Apr 2025 10:21:20 +0000
Subject: [PATCH 2/3] formatted Installer's Program.cs

---
 Installer/Program.cs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Installer/Program.cs b/Installer/Program.cs
index eeb65d5..092cc93 100644
--- a/Installer/Program.cs
+++ b/Installer/Program.cs
@@ -382,9 +382,9 @@ private static int BuildBundle(BootstrapperOptions opts)
                 // There is no license agreement for this SDK.
                 InstallArguments = "--quiet",
                 Vital = false,
-                Payloads = 
+                Payloads =
                 [
-                    new ExePackagePayload 
+                    new ExePackagePayload
                     {
                         SourceFile = opts.WindowsAppSdkPath
                     }

From 90d5fece965b89ac3b9eef9bd9ca8b39cefa2955 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <michael.suchacz@gmail.com>
Date: Tue, 1 Apr 2025 10:30:54 +0000
Subject: [PATCH 3/3] removed unused declaration

---
 Installer/Program.cs | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/Installer/Program.cs b/Installer/Program.cs
index 092cc93..78965e4 100644
--- a/Installer/Program.cs
+++ b/Installer/Program.cs
@@ -345,11 +345,6 @@ private static int BuildBundle(BootstrapperOptions opts)
         if (!DotNetRuntimePackagePayloads.TryGetValue(opts.Platform, out var dotNetRuntimePayload))
             throw new ArgumentException($"Invalid architecture '{opts.Platform}' specified", nameof(opts.Platform));
 
-        var windowsAppSdkPaylod = new ExePackagePayload
-        {
-            SourceFile = opts.WindowsAppSdkPath
-        };
-
         var bundle = new Bundle(ProductName,
             new ExePackage // .NET Runtime
             {