mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
Remove unused dependencies and shrink installer size (#47233)
## Summary of the Pull Request Two related installer changes to (1) eliminate genuinely-unused dependencies and (2) deduplicate shared WinAppSDK files between `<install>\` and `<install>\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 `<install>\<name>` into `<install>\WinUI3Apps\<name>` 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 `<install>\<file>` and `<install>\WinUI3Apps\<file>`. 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>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <ProjectTelemetry.h>
|
||||
#include <spdlog/sinks/base_sink.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string_view>
|
||||
|
||||
#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<size_t>(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<size_t>(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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user