From e932fe6e61c5697bb1b4b0ced16ab65abea68bbe Mon Sep 17 00:00:00 2001 From: Boliang Zhang <122517415+LegendaryBlair@users.noreply.github.com> Date: Fri, 15 May 2026 17:15:11 +0800 Subject: [PATCH] Remove unused dependencies and shrink installer size (#47233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request Two related installer changes to (1) eliminate genuinely-unused dependencies and (2) deduplicate shared WinAppSDK files between `\` and `\WinUI3Apps\` to shrink the installer download. ### 1. Remove unused dependencies (~11 MB savings per output location) - **System.Data.SqlClient**: Removed from MouseWithoutBorders projects and the central `Directory.Packages.props` pin. It was a transitive dependency of `Microsoft.Windows.Compatibility` but PowerToys has zero SQL database usage. - **Unused `using` import**: Removed `using System.ServiceModel.Channels` from MouseWithoutBorders `Program.cs` (no WCF usage). - **MFC / C++ AMP / OpenMP DLLs**: Added `RemoveUnusedVCRuntimeDlls` target in `Directory.Build.targets` to clean up `mfc140*`, `mfcm140*`, `vcamp140*`, and `vcomp140*` DLLs that leak from the VC++ Redistributable tree but are not imported by any PowerToys binary (verified with `dumpbin /dependents` across all installed binaries). Also excluded MFC DLLs from installer file collection. ### 2. WinAppSDK file deduplication (build-time only; install-time uses copy) **Background**: The `WinUI3Apps` subfolder must remain a real directory because MSIX sparse package registration applies DACL changes to the `ExternalLocation` folder (PR #47177). Flattening is not viable. **Build-time** (`generateAllFileComponents.ps1`): computes the SHA256 intersection of root and `WinUI3Apps` files, and for each file that is also present in the BaseApplications WXS file list, removes the duplicate from the WinUI3Apps WXS component list and writes its name to a `hardlinks.txt` manifest. The BaseApplications cross-check ensures we never deduplicate a file the MSI does not actually deploy at the install root, which would otherwise leave both copies missing post-install. The manifest is written as UTF-8 without BOM (via `[System.IO.File]::WriteAllLines` with `UTF8Encoding($false)`) so its encoding is identical regardless of the build host's PowerShell version. This step produces the **MSI download-size win** (~97 MB smaller cab; LZX:21 was already deduplicating most byte-identical content automatically inside the cab). **Install-time** (`CreateWinAppSDKHardlinksCA` custom action): - Reads `hardlinks.txt` after `InstallFiles` as a raw byte stream and converts each line to a `std::wstring` via `MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, ...)`. Avoids `std::wifstream`'s ANSI-codepage codecvt so non-ASCII paths can never be silently mangled. - For each entry, computes `(installDir / fileName).lexically_normal()` and `(winui3Dir / fileName).lexically_normal()`, then verifies via `std::mismatch` that each resolved path is still rooted at its respective folder. Manifest entries containing `..`, absolute paths, or alternate-stream syntax are logged and skipped. - Materialises each validated entry from `\` into `\WinUI3Apps\` via `fs::copy_file` (overwrite_existing). - Reports the per-file copy / failure counts to the install log. If every entry failed (`created == 0 && failed > 0`), the CA escalates to `E_FAIL` so the install does not silently succeed with an unusable WinUI3Apps tree. `DeleteWinAppSDKHardlinksCA` removes the materialised copies before `RemoveFiles` on uninstall, using the same UTF-8 reader and per-entry containment check. **WiX sequencing**: `CreateWinAppSDKHardlinks` runs `After="InstallFiles"` with `Condition="NOT Installed OR WIX_UPGRADE_DETECTED OR REINSTALL"` so a `msiexec /fa` repair refreshes the deduplicated copies (otherwise `RemoveFiles` would orphan them). #### Why copy and not hard-link A hard-linked variant of this CA was originally proposed but caused a Monaco preview-handler regression. Hard-links share an NTFS inode (and therefore one DACL) between `\` and `\WinUI3Apps\`. The MSIX sparse-package registrations for PowerRename / ImageResizer / FileLocksmith / NewPlus run after the dedup CA and propagate the `WinUI3Apps` parent's rich DACL (Capability SID, 5× Package SIDs, 5× conditional SYSAPPID ACE, RC SID) onto the shared inode. The root path then also exposes the rich DACL, which trips a kernel "stricter access evaluation" path that blocks the LOW-IL `prevhost.exe` from `LoadLibrary`-ing `hostfxr.dll` (and the rest of the .NET runtime), turning the Monaco preview pane blank for `.json` / `.md` / `.cs` / `.xaml` / `.svg` / `.xml` files. `fs::copy_file` creates a **fresh inode** for the WinUI3Apps copy. The root inode keeps its simple DACL (`SY:F + BA:F + owner:F` + inherited `BU:RX`) so LOW-IL `prevhost.exe` can still load it — Monaco preview works. The WinUI3Apps copy inherits the WinUI3Apps parent's rich DACL via normal NTFS inheritance (matches 0.99.1 behaviour exactly) — MSIX context-menu shells continue to work. #### Trade-off | Metric | Hard-link variant (rejected) | This PR (file copy) | 0.99.1 (no dedup) | |---|---|---|---| | MSI size | ~296 MB | ~296 MB | ~393 MB | | On-disk after install | ~2,475 MB | ~2,772 MB | ~2,772 MB | | DACL contamination risk | YES (broke Monaco) | NO | NO | The on-disk savings (~297 MB) are given up in exchange for eliminating the DACL contamination risk; the **installer download savings (~97 MB)** are preserved by the build-time WiX/cab dedup. #### Edge cases handled - Empty duplicate list: `hardlinks.txt` always written, CA handles empty. - All files duplicated: `Generate-FileComponents` returns early for empty list. - File stripped from BaseApplications by an earlier build step: BaseApplications cross-check skips it during dedup so neither copy goes missing. - Manifest entry escapes install root (`..`, absolute path): rejected per-entry, install continues. - Manifest line is non-UTF-8: rejected per-entry, install continues. - Source missing at install time: per-entry skip, install continues. - All copies fail: install aborts loudly via `E_FAIL` (catastrophic-case escalation). - Upgrade or `msiexec /fa` repair: CA fires (`NOT Installed OR WIX_UPGRADE_DETECTED OR REINSTALL`). **MSI repair risk**: Burn bundle uses `SuppressRepair=yes` and `MajorUpgrade` (full uninstall + reinstall) for all version upgrades, so the standard upgrade path is unaffected. The `OR REINSTALL` clause covers power users running `msiexec /fa` directly. ## PR Checklist - [x] **Communication:** Discussed approach via PRs #46866, #47177, #46745 - [ ] **Tests:** Installer infrastructure only — no runtime behaviour changes - [ ] **Localization:** N/A - [ ] **Dev docs:** N/A - [ ] **New binaries:** N/A ## Detailed Description of the Pull Request / Additional comments Based on the approach from PR #46745 by @yeelam-gordon, rebased onto latest main and switched from hard-links to file copies after the DACL contamination root cause was identified. Hardening (UTF-8 read, path containment, catastrophic-case escalation, REINSTALL repair, BaseApplications-filtered dedup) added in response to review feedback. These changes are purely build/installer infrastructure — no runtime behaviour changes to any PowerToys module. ## Validation Steps Performed Validated on a 0.99.4 / 0.99.5 local install (per-user `%LocalAppData%\PowerToys`): - ✅ `dumpbin /dependents` across the installed PowerToys tree confirmed zero binaries import `mfc140*`, `mfcm140*`, `vcamp140*`, or `vcomp140*` — the cleanup target removes ~11 MB of genuinely unused VC runtime DLLs. - ✅ `System.Data.SqlClient` has zero call-sites in PowerToys source. - ✅ Local installer build produces a 296 MB MSI (down from 393 MB pre-dedup, ~97 MB cab savings purely from the build-time WiX dedup). - ✅ MSI table inspection (`wix msi decompile`) confirms the deferred CAs are present (`CreateWinAppSDKHardlinks`, `DeleteWinAppSDKHardlinks`) and the `hardlinks.txt` File row is registered. - ✅ MSI table inspection confirms .NET runtime DLLs (`hostfxr.dll`, `coreclr.dll`, `hostpolicy.dll`, `clretwrc.dll`, `Accessibility.dll`, `backup_restore_settings.json`) appear ONLY in `BaseApplicationsFiles_File_*`, NOT in `WinUI3ApplicationsFiles_File_*` — proving the build-time dedup worked. - ✅ Post-install verification: deduplicated files materialised at both root and WinUI3Apps with byte-identical SHA256 hashes, and `fsutil hardlink list` returns link-count == 1 for each — proving the install-time copy approach worked, not hard-link. - ✅ DACL on root .NET runtime DLLs is clean: no Package SID, no Capability SID, no SYSAPPID conditional ACE, no `ALL APPLICATION PACKAGES` ACE — Monaco preview load path is safe. - ✅ DACL on WinUI3Apps copies has the rich MSIX inheritance — context-menu shells continue to work (matches 0.99.1). - ✅ All four MSIX sparse packages (PowerRename, ImageResizer, FileLocksmith, NewPlus) registered after install. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/spell-check/expect.txt | 3 + Directory.Build.targets | 16 ++ Directory.Packages.props | 2 - .../CustomAction.cpp | 218 ++++++++++++++++++ .../CustomAction.def | 2 + installer/PowerToysSetupVNext/Product.wxs | 6 + .../WinUI3Applications.wxs | 7 + .../generateAllFileComponents.ps1 | 72 +++++- .../MouseWithoutBorders/App/Class/Program.cs | 1 - .../Helper/MouseWithoutBordersHelper.csproj | 1 - .../App/MouseWithoutBorders.csproj | 1 - .../Service/MouseWithoutBordersService.csproj | 1 - 12 files changed, 323 insertions(+), 7 deletions(-) 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 @@ -