From 6ecbcc2755a04c90ee228bd32c5dc94f6aa99c67 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 2 Apr 2026 15:19:54 -0500 Subject: [PATCH 1/7] Add default installer precedence if not defined by user --- .../ManifestComparator.cpp | 31 +++++++++++++++++-- .../Manifest/ManifestComparator.cpp | 16 ++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/AppInstallerCLITests/ManifestComparator.cpp b/src/AppInstallerCLITests/ManifestComparator.cpp index 9a7f08db01..4edbaccaf7 100644 --- a/src/AppInstallerCLITests/ManifestComparator.cpp +++ b/src/AppInstallerCLITests/ManifestComparator.cpp @@ -199,8 +199,8 @@ TEST_CASE("ManifestComparator_InstalledTypeFilter", "[manifest_comparator]") ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); - // Only because it is first - RequireInstaller(result, msi); + // Msix is preferred over Msi by the default installer type precedence order + RequireInstaller(result, msix); REQUIRE(inapplicabilities.size() == 0); } SECTION("MSI Installed") @@ -238,7 +238,7 @@ TEST_CASE("ManifestComparator_InstalledTypeCompare", "[manifest_comparator]") ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); - // Only because it is first + // Burn is preferred over Exe by the default installer type precedence order RequireInstaller(result, burn); REQUIRE(inapplicabilities.size() == 0); } @@ -855,6 +855,31 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") REQUIRE(!result); RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); } + SECTION("No user preference - default order applies") + { + // No TestUserSettings means no user-configured preferences; the default order should be used. + // Default order: MSStore > Msix > Msi > Wix > Burn > Nullsoft > Inno > Exe > Portable + // Manifest has Msi, Exe, Msix — Msix should win. + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msix); + REQUIRE(inapplicabilities.size() == 0); + } + SECTION("No user preference - type not in default list does not win over default-ordered types") + { + // Zip is not in the default preference list; it should not be preferred over Msix/Msi/Exe. + Manifest localManifest; + ManifestInstaller zip = AddInstaller(localManifest, Architecture::Neutral, InstallerTypeEnum::Zip, ScopeEnum::User); + ManifestInstaller localExe = AddInstaller(localManifest, Architecture::Neutral, InstallerTypeEnum::Exe, ScopeEnum::User); + + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(localManifest); + + // Exe is in the default list; Zip is not — Exe should be preferred. + RequireInstaller(result, localExe); + REQUIRE(inapplicabilities.size() == 0); + } } TEST_CASE("ManifestComparator_MachineArchitecture_Strong_Scope_Weak", "[manifest_comparator]") diff --git a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp index 92bd27f0aa..6a5bf44758 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp @@ -228,6 +228,22 @@ namespace AppInstaller::Manifest { preference = Settings::User().Get(); requirement = Settings::User().Get(); + + // Apply default precedence order when the user has not configured any installer type preferences or requirements. + if (preference.empty() && requirement.empty()) + { + preference = { + InstallerTypeEnum::MSStore, + InstallerTypeEnum::Msix, + InstallerTypeEnum::Msi, + InstallerTypeEnum::Wix, + InstallerTypeEnum::Burn, + InstallerTypeEnum::Nullsoft, + InstallerTypeEnum::Inno, + InstallerTypeEnum::Exe, + InstallerTypeEnum::Portable, + }; + } } if (!preference.empty() || !requirement.empty()) From 8f6eba1aecc32bd6ae346d6412284129e5834de4 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Thu, 2 Apr 2026 15:32:09 -0500 Subject: [PATCH 2/7] Update Release Notes --- doc/ReleaseNotes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 6b2029d84d..6339d435bf 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -47,6 +47,14 @@ The WinGet MCP server's existing tools have been extended with new parameters to The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` environment variables to authenticate GitHub API requests. This significantly increases the GitHub API rate limit, preventing failures in CI/CD pipelines. Use `-Verbose` to see which token is being used. +### Default priority of installer types + +Installer type selection no longer depends on the order defined on the manifest. Instead, preference is given in this order: +- MSIX +- MSI / Wix / Burn +- Nullsoft / Inno / EXE +- Portable + ## Bug Fixes * Fixed the `useLatest` property in the DSC v3 `Microsoft.WinGet/Package` resource schema to emit a boolean default (`false`) instead of the incorrect string `"false"`. From f88311be32e4f804d1e3ac6dc656bf75bdf963ea Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Mon, 6 Apr 2026 16:54:51 -0500 Subject: [PATCH 3/7] Empty commit for spell check From 8fe547edbd218392541d74050de2321b708d9dde Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 28 Apr 2026 08:07:15 -0500 Subject: [PATCH 4/7] Make requirements influence order --- doc/ReleaseNotes.md | 2 + doc/Settings.md | 7 +++- .../ManifestComparator.cpp | 38 +++++++++++++++++++ .../Manifest/ManifestComparator.cpp | 9 ++++- 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index 7aad84e6ac..f7185e058a 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -55,6 +55,8 @@ Installer type selection no longer depends on the order defined on the manifest. - Nullsoft / Inno / EXE - Portable +When a user configures installer type requirements or preferences, the order in which they are listed is now respected during installer selection. Requirements take precedence over preferences when both are configured. + ### Improved `list` output when redirected - `winget list` (and similar table commands) no longer truncates output when stdout is redirected to a file or variable — column widths are now computed from the full result set. diff --git a/doc/Settings.md b/doc/Settings.md index 4915f81afa..90f6c97daa 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -137,6 +137,8 @@ The `archiveExtractionMethod` behavior affects how installer archives are extrac Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. +When multiple values are listed under `requirements`, they are treated as an ordered preference in addition to a filter — the first listed value is preferred over subsequent ones when multiple valid options exist. If both `requirements` and `preferences` are set for the same field, the ordering from `requirements` takes precedence. + Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command. > [!NOTE] @@ -186,12 +188,15 @@ The `installerTypes` behavior affects what installer types will be selected when Allowed values as of version 1.12.470 include: `appx`, `burn`, `exe`, `font`, `inno`, `msi`, `msix`, `msstore`, `nullsoft`, `portable`, `wix`, `zip` -By default, and with all other properties being equal, WinGet defaults to the installer type that is listed first in the manifest's installer YAML if the package has not been installed yet. If it is already installed, the same installer type will be required to ensure a proper upgrade. +By default, when no user preference or requirement is configured, WinGet selects installer types in the following order: MSIX, MSI/Wix/Burn, Nullsoft/Inno/EXE, Portable. If it is already installed, the same installer type will be required to ensure a proper upgrade. ```json "installBehavior": { "preferences": { "installerTypes": ["msi", "msix"] + }, + "requirements": { + "installerTypes": ["msix", "msi"] } }, ``` diff --git a/src/AppInstallerCLITests/ManifestComparator.cpp b/src/AppInstallerCLITests/ManifestComparator.cpp index 4edbaccaf7..b67f618bfc 100644 --- a/src/AppInstallerCLITests/ManifestComparator.cpp +++ b/src/AppInstallerCLITests/ManifestComparator.cpp @@ -844,6 +844,44 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") RequireInstaller(result, exe); RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType, InapplicabilityFlags::InstallerType }); } + SECTION("Multiple requirements - first requirement wins") + { + // Requirements act as both a filter and an ordering guide; Exe is listed first so it wins. + TestUserSettings settings; + settings.Set({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix }); + + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, exe); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType }); + } + SECTION("Multiple requirements alternate order") + { + // Same requirements as above but Msix is listed first — Msix should win. + TestUserSettings settings; + settings.Set({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe }); + + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, msix); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType }); + } + SECTION("Requirements and preferences coexist - requirement ordering wins") + { + // When both are set, requirement ordering takes precedence over preference ordering. + // Requirements say Exe first; preferences say Msix first — Exe should win. + TestUserSettings settings; + settings.Set({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix }); + settings.Set({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe }); + + ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); + auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); + + RequireInstaller(result, exe); + RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType }); + } SECTION("Inno requirement") { TestUserSettings settings; diff --git a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp index 6a5bf44758..df3f830d19 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp @@ -286,12 +286,17 @@ namespace AppInstaller::Manifest details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override { - if (m_preference.empty()) + // Requirements take precedence over preferences for ordering; fall back to preferences when no requirements are set. + const auto& effectiveOrder = !m_requirement.empty() ? m_requirement : m_preference; + + // There is a default order so this branch should never be hit + // However, if by some miracle it is, we won't consider either installer as better than the other based on installer type. + if (effectiveOrder.empty()) { return details::ComparisonResult::Negative; } - for (InstallerTypeEnum installerTypePreference : m_preference) + for (InstallerTypeEnum installerTypePreference : effectiveOrder) { bool isFirstInstallerTypePreferred = first.EffectiveInstallerType() == installerTypePreference || From e226735506ad673d2645c57059f1ca4bf1e72bb9 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 28 Apr 2026 08:16:21 -0500 Subject: [PATCH 5/7] Remove inaccurate comment --- src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp index df3f830d19..831ce93f19 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp @@ -289,8 +289,6 @@ namespace AppInstaller::Manifest // Requirements take precedence over preferences for ordering; fall back to preferences when no requirements are set. const auto& effectiveOrder = !m_requirement.empty() ? m_requirement : m_preference; - // There is a default order so this branch should never be hit - // However, if by some miracle it is, we won't consider either installer as better than the other based on installer type. if (effectiveOrder.empty()) { return details::ComparisonResult::Negative; From 7e6c7a525e9d44ccd42ef3bb0af19c86754aa927 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 28 Apr 2026 17:19:07 -0500 Subject: [PATCH 6/7] Make preference take precedence Co-authored-by: JohnMcPMS --- doc/ReleaseNotes.md | 2 +- doc/Settings.md | 2 +- src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/ReleaseNotes.md b/doc/ReleaseNotes.md index f7185e058a..97dfd9c371 100644 --- a/doc/ReleaseNotes.md +++ b/doc/ReleaseNotes.md @@ -55,7 +55,7 @@ Installer type selection no longer depends on the order defined on the manifest. - Nullsoft / Inno / EXE - Portable -When a user configures installer type requirements or preferences, the order in which they are listed is now respected during installer selection. Requirements take precedence over preferences when both are configured. +When a user configures installer type requirements or preferences, the order in which they are listed is now respected during installer selection. ### Improved `list` output when redirected diff --git a/doc/Settings.md b/doc/Settings.md index 90f6c97daa..189761ec39 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -137,7 +137,7 @@ The `archiveExtractionMethod` behavior affects how installer archives are extrac Some of the settings are duplicated under `preferences` and `requirements`. `preferences` affect how the various available options are sorted when choosing the one to act on. For instance, the default scope of package installs is for the current user, but if that is not an option then a machine level installer will be chosen. `requirements` filter the options, potentially resulting in an empty list and a failure to install. In the previous example, a user scope requirement would result in no applicable installers and an error. -When multiple values are listed under `requirements`, they are treated as an ordered preference in addition to a filter — the first listed value is preferred over subsequent ones when multiple valid options exist. If both `requirements` and `preferences` are set for the same field, the ordering from `requirements` takes precedence. +When multiple values are listed under `requirements`, they are treated as an ordered preference in addition to a filter — the first listed value is preferred over subsequent ones when multiple valid options exist. If both `requirements` and `preferences` are set for the same field, the ordering from `preferences` takes precedence. Any arguments passed on the command line will effectively override the matching `requirement` setting for the duration of that command. diff --git a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp index 831ce93f19..819b72cffb 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestComparator.cpp @@ -286,8 +286,8 @@ namespace AppInstaller::Manifest details::ComparisonResult IsFirstBetter(const ManifestInstaller& first, const ManifestInstaller& second) override { - // Requirements take precedence over preferences for ordering; fall back to preferences when no requirements are set. - const auto& effectiveOrder = !m_requirement.empty() ? m_requirement : m_preference; + // If no preferences are set, use requirement ordering instead. + const auto& effectiveOrder = m_preference.empty() ? m_requirement : m_preference; if (effectiveOrder.empty()) { From b6c7672860f5c1d8cd8f3d567d47a7690ef6c5f3 Mon Sep 17 00:00:00 2001 From: Kaleb Luedtke Date: Tue, 28 Apr 2026 17:21:26 -0500 Subject: [PATCH 7/7] Update test --- src/AppInstallerCLITests/ManifestComparator.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AppInstallerCLITests/ManifestComparator.cpp b/src/AppInstallerCLITests/ManifestComparator.cpp index b67f618bfc..5555c52eec 100644 --- a/src/AppInstallerCLITests/ManifestComparator.cpp +++ b/src/AppInstallerCLITests/ManifestComparator.cpp @@ -868,10 +868,10 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") RequireInstaller(result, msix); RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType }); } - SECTION("Requirements and preferences coexist - requirement ordering wins") + SECTION("Requirements and preferences coexist - preference ordering wins") { - // When both are set, requirement ordering takes precedence over preference ordering. - // Requirements say Exe first; preferences say Msix first — Exe should win. + // When both are set, preference ordering takes precedence over requirement ordering. + // Preferences say Msix first; requirements say Exe first — Msix should win. TestUserSettings settings; settings.Set({ InstallerTypeEnum::Exe, InstallerTypeEnum::Msix }); settings.Set({ InstallerTypeEnum::Msix, InstallerTypeEnum::Exe }); @@ -879,7 +879,7 @@ TEST_CASE("ManifestComparator_InstallerType", "[manifest_comparator]") ManifestComparator mc(GetManifestComparatorOptions(ManifestComparatorTestContext{}, {})); auto [result, inapplicabilities] = mc.GetPreferredInstaller(manifest); - RequireInstaller(result, exe); + RequireInstaller(result, msix); RequireInapplicabilities(inapplicabilities, { InapplicabilityFlags::InstallerType }); } SECTION("Inno requirement")