diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 20f8466f6b..a81f5862e2 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -440,6 +440,7 @@ DString
DSVG
dto
DUMMYUNIONNAME
+dumpbin
dutil
DVASPECT
DVASPECTINFO
@@ -710,6 +711,7 @@ HOOKPROC
HORZRES
HORZSIZE
Hostbackdropbrush
+hostfxr
hostsfileeditor
hotfixes
hotkeycontrol
@@ -1022,6 +1024,7 @@ Metadatas
metafile
metapackage
mfc
+mfcm
Mgmt
Microwaved
middleclickaction
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 9efab5a9a5..4ba887527a 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -65,4 +65,20 @@
+
+
+
+
+ <_UnusedVCRuntimeDlls Include="$(OutDir)mfc140*.dll" />
+ <_UnusedVCRuntimeDlls Include="$(OutDir)mfcm140*.dll" />
+ <_UnusedVCRuntimeDlls Include="$(OutDir)vcamp140*.dll" />
+ <_UnusedVCRuntimeDlls Include="$(OutDir)vcomp140*.dll" />
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0650b78e61..42282b5959 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -110,8 +110,6 @@
-
-
diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
index 890a9fdf6e..4a83582dc2 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include
#include "../../src/common/logger/logger.h"
@@ -1807,6 +1808,223 @@ void initSystemLogger()
} });
}
+// Naming note: the *Hardlinks* names in this CA, the matching WiX CustomAction Ids
+// in Product.wxs, and the manifest filename "hardlinks.txt" are kept for continuity
+// with the original PR design. The implementation uses fs::copy_file -- not
+// CreateHardLinkW -- because hard-links share an inode (and DACL) between root and
+// WinUI3Apps, which lets MSIX sparse-package registration propagate a rich DACL onto
+// the root copy of files like hostfxr.dll and break LOW-IL prevhost.exe loads,
+// turning the Monaco preview pane blank. Copies create a fresh inode in WinUI3Apps so the root
+// copy keeps its simple DACL. See the in-body comment for the full RCA reference.
+UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall)
+{
+ HRESULT hr = S_OK;
+ UINT er = ERROR_SUCCESS;
+ std::wstring installationFolder;
+
+ hr = WcaInitialize(hInstall, "CreateWinAppSDKHardlinks");
+ ExitOnFailure(hr, "Failed to initialize");
+ hr = getInstallFolder(hInstall, installationFolder);
+ ExitOnFailure(hr, "Failed to get installFolder.");
+
+ {
+ namespace fs = std::filesystem;
+ const fs::path installDir(installationFolder);
+ const fs::path winui3Dir = installDir / L"WinUI3Apps";
+ const fs::path manifestPath = winui3Dir / L"hardlinks.txt";
+
+ if (!fs::exists(manifestPath))
+ {
+ WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: No hardlinks.txt manifest found, skipping.");
+ goto LExit;
+ }
+
+ std::ifstream manifestFile(manifestPath); // Read as bytes, then convert UTF-8 -> wide explicitly.
+ std::string narrowLine;
+ int created = 0;
+ int failed = 0;
+
+ // INSTALLFOLDER from MSI typically arrives with a trailing backslash. lexically_normal
+ // preserves that as an empty trailing path component, which would later make the
+ // per-component std::mismatch containment check below reject every legitimate entry.
+ // Strip any trailing separators before normalizing.
+ auto stripTrailingSep = [](fs::path p) {
+ auto s = p.native();
+ while (s.size() > 1 && (s.back() == L'\\' || s.back() == L'/')) s.pop_back();
+ return fs::path(s);
+ };
+
+ // Normalize once so the per-line containment check below is cheap.
+ const fs::path installDirNorm = stripTrailingSep(installDir).lexically_normal();
+ const fs::path winui3DirNorm = stripTrailingSep(winui3Dir).lexically_normal();
+
+ while (std::getline(manifestFile, narrowLine))
+ {
+ if (narrowLine.empty())
+ {
+ continue;
+ }
+ // Strip CR if the manifest uses CRLF line endings.
+ if (narrowLine.back() == '\r')
+ {
+ narrowLine.pop_back();
+ if (narrowLine.empty()) continue;
+ }
+
+ // Manifest is written as UTF-8 (no BOM) -- convert to wide string explicitly
+ // rather than relying on the locale-default codecvt of std::wifstream, which is
+ // the ANSI code page on Windows and would silently mangle any non-ASCII path.
+ const int wideLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, nullptr, 0);
+ if (wideLen <= 0)
+ {
+ WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Skipping non-UTF-8 entry: %hs", narrowLine.c_str());
+ failed++;
+ continue;
+ }
+ std::wstring fileName(static_cast(wideLen) - 1, L'\0');
+ MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, fileName.data(), wideLen);
+
+ // Defense-in-depth: reject manifest entries that would escape the install root
+ // via "..", absolute paths, or alternate stream syntax. lexically_normal collapses
+ // any "." / ".." / repeated separators, then std::mismatch verifies the resolved
+ // path is still rooted at installDir / winui3Dir respectively.
+ const fs::path source = (installDir / fileName).lexically_normal();
+ const fs::path target = (winui3Dir / fileName).lexically_normal();
+ const auto sourceIn = std::mismatch(installDirNorm.begin(), installDirNorm.end(), source.begin(), source.end());
+ const auto targetIn = std::mismatch(winui3DirNorm.begin(), winui3DirNorm.end(), target.begin(), target.end());
+ if (sourceIn.first != installDirNorm.end() || targetIn.first != winui3DirNorm.end())
+ {
+ WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Rejecting entry outside install root: %ls", fileName.c_str());
+ failed++;
+ continue;
+ }
+
+ if (!fs::exists(source))
+ {
+ WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Source not found: %ls", source.c_str());
+ failed++;
+ continue;
+ }
+
+ // Remove existing file if present (leftover from previous install)
+ std::error_code ec;
+ fs::remove(target, ec);
+
+ // Use a regular file copy (not a hard-link). Hard-links share an
+ // NTFS inode -- and therefore one DACL -- between root and
+ // WinUI3Apps, which lets MSIX sparse-package registration
+ // propagate the WinUI3Apps parent's rich (Capability/Package SID)
+ // DACL onto the root path. That trips a kernel "stricter access
+ // evaluation" path that blocks LOW-IL prevhost.exe from loading
+ // hostfxr.dll, so File Explorer Monaco preview goes blank on
+ // Windows 11 23H2. Copying creates a fresh inode in WinUI3Apps,
+ // so the root copy keeps its simple DACL while the WinUI3Apps
+ // copy inherits the rich DACL from its parent (matches 0.99.1
+ // behaviour). See Documents\PR-47233-Handoff.md for full RCA.
+ fs::copy_file(source, target, fs::copy_options::overwrite_existing, ec);
+ if (ec)
+ {
+ WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Failed to copy: %ls (%hs)", fileName.c_str(), ec.message().c_str());
+ failed++;
+ }
+ else
+ {
+ created++;
+ }
+ }
+
+ WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Copied %d files, %d failures", created, failed);
+
+ // Catastrophic-case escalation: if every copy failed, the WinUI3Apps tree is
+ // unusable (Monaco preview / context-menu shells will break). Surface this rather
+ // than reporting install success. Per-file failures remain tolerated.
+ if (created == 0 && failed > 0)
+ {
+ hr = E_FAIL;
+ ExitOnFailure(hr, "All WinAppSDK file copies failed; aborting install.");
+ }
+ }
+
+LExit:
+ er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
+ return WcaFinalize(er);
+}
+
+UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall)
+{
+ HRESULT hr = S_OK;
+ UINT er = ERROR_SUCCESS;
+ std::wstring installationFolder;
+
+ hr = WcaInitialize(hInstall, "DeleteWinAppSDKHardlinks");
+ ExitOnFailure(hr, "Failed to initialize");
+ hr = getInstallFolder(hInstall, installationFolder);
+ ExitOnFailure(hr, "Failed to get installFolder.");
+
+ {
+ namespace fs = std::filesystem;
+ const fs::path winui3Dir = fs::path(installationFolder) / L"WinUI3Apps";
+ const fs::path manifestPath = winui3Dir / L"hardlinks.txt";
+
+ if (!fs::exists(manifestPath))
+ {
+ goto LExit;
+ }
+
+ std::ifstream manifestFile(manifestPath); // Read as bytes; convert UTF-8 -> wide explicitly.
+ std::string narrowLine;
+
+ // INSTALLFOLDER from MSI typically arrives with a trailing backslash; strip it before
+ // normalizing so the per-line containment check doesn't false-reject every entry.
+ auto stripTrailingSep = [](fs::path p) {
+ auto s = p.native();
+ while (s.size() > 1 && (s.back() == L'\\' || s.back() == L'/')) s.pop_back();
+ return fs::path(s);
+ };
+ const fs::path winui3DirNorm = stripTrailingSep(winui3Dir).lexically_normal();
+
+ while (std::getline(manifestFile, narrowLine))
+ {
+ if (narrowLine.empty())
+ {
+ continue;
+ }
+ if (narrowLine.back() == '\r')
+ {
+ narrowLine.pop_back();
+ if (narrowLine.empty()) continue;
+ }
+
+ const int wideLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, nullptr, 0);
+ if (wideLen <= 0)
+ {
+ WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Skipping non-UTF-8 entry: %hs", narrowLine.c_str());
+ continue;
+ }
+ std::wstring fileName(static_cast(wideLen) - 1, L'\0');
+ MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, fileName.data(), wideLen);
+
+ // Defense-in-depth: reject entries whose resolved target escapes WinUI3Apps.
+ const fs::path target = (winui3Dir / fileName).lexically_normal();
+ const auto inWinui3 = std::mismatch(winui3DirNorm.begin(), winui3DirNorm.end(), target.begin(), target.end());
+ if (inWinui3.first != winui3DirNorm.end())
+ {
+ WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Rejecting entry outside WinUI3Apps: %ls", fileName.c_str());
+ continue;
+ }
+
+ std::error_code ec;
+ fs::remove(target, ec);
+ }
+
+ WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Cleaned up deduplicated copy files");
+ }
+
+LExit:
+ er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
+ return WcaFinalize(er);
+}
+
// DllMain - Initialize and cleanup WiX custom action utils.
extern "C" BOOL WINAPI DllMain(__in HINSTANCE hInst, __in ULONG ulReason, __in LPVOID)
{
diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
index 86efe34aa6..1b8b7d55f8 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
@@ -36,3 +36,5 @@ EXPORTS
SetBundleInstallLocationCA
InstallPackageIdentityMSIXCA
UninstallPackageIdentityMSIXCA
+ CreateWinAppSDKHardlinksCA
+ DeleteWinAppSDKHardlinksCA
diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs
index 74a09972d6..8651a7d83d 100644
--- a/installer/PowerToysSetupVNext/Product.wxs
+++ b/installer/PowerToysSetupVNext/Product.wxs
@@ -112,6 +112,8 @@
+
+
@@ -124,6 +126,7 @@
+
@@ -137,6 +140,7 @@
+
@@ -189,8 +193,10 @@
+
+
diff --git a/installer/PowerToysSetupVNext/WinUI3Applications.wxs b/installer/PowerToysSetupVNext/WinUI3Applications.wxs
index 4c177b960a..c0d3afb6f4 100644
--- a/installer/PowerToysSetupVNext/WinUI3Applications.wxs
+++ b/installer/PowerToysSetupVNext/WinUI3Applications.wxs
@@ -7,11 +7,18 @@
+
+
+
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
index 048f587def..7273251844 100644
--- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
+++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
@@ -30,6 +30,10 @@ Function Generate-FileList() {
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri")
+ # MFC DLLs leak into the output via WindowsAppSDKSelfContained but no PowerToys binary imports them.
+ # Verified with dumpbin /dependents across all 2176 binaries — zero consumers.
+ $fileExclusionList += @("mfc140.dll", "mfc140u.dll", "mfcm140.dll", "mfcm140u.dll")
+
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")
if ($fileDepsJson -eq [string]::Empty) {
@@ -85,11 +89,16 @@ Function Generate-FileComponents() {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'fileList',
Justification = 'variable is used in another scope')]
- $fileList = $matches[2] -split ';'
+ $fileList = $matches[2] -split ';' | Where-Object { $_ -ne '' }
return
}
}
+ if ($null -eq $fileList -or $fileList.Count -eq 0) {
+ # No files to generate components for — leave placeholder intact
+ return
+ }
+
$componentId = "$($fileListName)_Component"
$componentDefs = "`r`n"
@@ -154,6 +163,67 @@ Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSSc
#WinUI3Applications
Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
+
+# Deduplicate: Remove files from WinUI3Apps that are identical to root (same name + same hash).
+# These will be re-created as plain file copies at install time by CreateWinAppSDKHardlinksCA.
+# (The CA's name is historical: it now uses fs::copy_file rather than CreateHardLinkW to avoid
+# DACL contamination across the shared inode -- see CustomAction.cpp for details.)
+$rootPath = "$PSScriptRoot..\..\..\$platform\Release"
+$winui3Path = "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
+$winui3WxsPath = "$PSScriptRoot\WinUI3Applications.wxs"
+$winui3Wxs = Get-Content $winui3WxsPath -Raw
+$manifestPath = Join-Path $winui3Path "hardlinks.txt"
+
+if ($winui3Wxs -match "\<\?define WinUI3ApplicationsFiles=([^?]*)\?\>") {
+ $winui3FileList = $matches[1] -split ';' | Where-Object { $_ -ne '' }
+ $hardlinkFiles = @()
+
+ # Read the BaseApplications WXS file list so we only deduplicate files that the MSI
+ # is actually deploying to the install root. If a file was stripped from BaseApplications
+ # by an earlier step (e.g., the ImageResizer leaked-apphost workaround above), the
+ # install-time CA's source would be missing and both copies would disappear.
+ $baseAppsWxs = Get-Content $baseAppWxsPath -Raw
+ $baseAppsFileList = @()
+ if ($baseAppsWxs -match "\<\?define BaseApplicationsFiles=([^?]*)\?\>") {
+ $baseAppsFileList = $matches[1] -split ';' | Where-Object { $_ -ne '' }
+ }
+
+ foreach ($file in $winui3FileList) {
+ # Skip files that were intentionally not deployed to root by the build
+ if ($baseAppsFileList -notcontains $file) { continue }
+
+ $rootFile = Join-Path $rootPath $file
+ $winui3File = Join-Path $winui3Path $file
+ if ((Test-Path $rootFile) -and (Test-Path $winui3File)) {
+ $rootHash = (Get-FileHash $rootFile -Algorithm SHA256).Hash
+ $winui3Hash = (Get-FileHash $winui3File -Algorithm SHA256).Hash
+ if ($rootHash -eq $winui3Hash) {
+ $hardlinkFiles += $file
+ }
+ }
+ }
+
+ if ($hardlinkFiles.Count -gt 0) {
+ # Remove deduplicated files from WinUI3Apps file list
+ $remainingFiles = $winui3FileList | Where-Object { $_ -notin $hardlinkFiles }
+ if ($remainingFiles.Count -eq 0) {
+ # All files are duplicates — keep at least a dummy entry won't be emitted
+ # Generate-FileComponents handles empty defines by producing no entries
+ $winui3Wxs = $winui3Wxs -replace "\<\?define WinUI3ApplicationsFiles=[^?]*\?\>", ""
+ } else {
+ $winui3Wxs = $winui3Wxs -replace "\<\?define WinUI3ApplicationsFiles=[^?]*\?\>", ""
+ }
+ Set-Content -Path $winui3WxsPath -Value $winui3Wxs
+ Write-Host "Deduplicated $($hardlinkFiles.Count) files from WinUI3Apps (will be copied at install time)"
+ }
+
+ # Always write hardlinks.txt (may be empty — CA handles that gracefully)
+ # Write as UTF-8 without BOM so the install-time CA can read it via std::ifstream
+ # + MultiByteToWideChar(CP_UTF8) without dealing with PS-version-dependent default
+ # encodings or a leading BOM.
+ [System.IO.File]::WriteAllLines($manifestPath, [string[]]$hardlinkFiles, (New-Object System.Text.UTF8Encoding($false)))
+}
+
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs
#AdvancedPaste
diff --git a/src/modules/MouseWithoutBorders/App/Class/Program.cs b/src/modules/MouseWithoutBorders/App/Class/Program.cs
index 144007e92f..129fba3ce3 100644
--- a/src/modules/MouseWithoutBorders/App/Class/Program.cs
+++ b/src/modules/MouseWithoutBorders/App/Class/Program.cs
@@ -21,7 +21,6 @@ using System.IO.Pipes;
using System.Linq;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Principal;
-using System.ServiceModel.Channels;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
diff --git a/src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj b/src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj
index 4b3fc7fd50..096e1745db 100644
--- a/src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj
+++ b/src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj
@@ -66,7 +66,6 @@
-
diff --git a/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj b/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj
index 675e927334..83906924fb 100644
--- a/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj
+++ b/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj
@@ -214,7 +214,6 @@
-
diff --git a/src/modules/MouseWithoutBorders/App/Service/MouseWithoutBordersService.csproj b/src/modules/MouseWithoutBorders/App/Service/MouseWithoutBordersService.csproj
index 0decd70d38..0f522b1bad 100644
--- a/src/modules/MouseWithoutBorders/App/Service/MouseWithoutBordersService.csproj
+++ b/src/modules/MouseWithoutBorders/App/Service/MouseWithoutBordersService.csproj
@@ -71,7 +71,6 @@
-